8 puan yazan GN⁺ 2023-08-20 | 1 yorum | WhatsApp'ta paylaş
  • Derleyici türü araçlar için uygulama dili geleneksel olarak OCaml ve C++ arasında bölünmüş olsa da, küçük dil deneylerinde TypeScript, ML ailesi diller gibi hafifçe kullanılabilecek bir seçenek hâline geliyor
  • Rust, ML ve C++’ın güçlü yanlarını birleştirip güvenli çok iş parçacıklılığı da sunsa da, verinin fiziksel yerleşimini modellemek gerektiği için küçük prototipler için yükü artabiliyor
  • Deno; tekil binary, yerleşik linting ve formatting, derleme adımının olmaması, task runner ve watch modu sayesinde TypeScript dil deneylerine hızlı başlamayı sağlıyor
  • Örnek typechecker; generic AST, tagged union, visitor ve bottom-up transform’u birleştirerek Expr<void>’u Expr<Type>’a dönüştürüyor, TypeError’ı bir tip değeri olarak koyarak zincirleme hataları azaltıyor
  • TypeScript; otomatik tamamlama, esnek tip sistemi ve gerektiğinde dinamik tarafa kaçabilen runtime özellikleri sayesinde küçük dil hack’leri için üretken bir araç olabilir

Uygulama dili seçiminin mevcut dengesi

  • Derleyici biçiminde araçlar geliştirirken uygulama dili seçiminde kabaca iki akım var
  • Uygulama odaklı ve production’a hazır olması gereken işlerde C++ sıkça seçiliyor
    • LLVM, clang, v8 ve HotSpot’un tamamı C++ tabanlı
  • Rust, ML ve C++’tan doğrudan etkilenmiş; iki dilin avantajlarını birleştirirken güvenli çok iş parçacıklılık gibi kendi güçlü yanlarını da sunuyor
    • Ancak spektrumda daha çok production’a hazır olma tarafına eğiliyor
    • “Öylece çalışan” build sistemi prototiplemeye de yardımcı olsa da, verinin fiziksel yerleşimini modelleme gerektiren ek karmaşıklık getiriyor

Rust’ta indeks yaklaşımının avantajları ve küçük kodlarda yükü

  • Rust ile derleyici yazarken sık verilen tavsiye, pointer’lardan kaçınmak ve indeks kullanmak
  • İndeksler büyük codebase’lerde çeşitli avantajlara sahip
    • Side table’lar ilgili modülün içinde tutulabildiği için coupling azalır
    • İndeks u32 olur ve struct-of-arrays yerleşimini teşvik ederek performans açısından avantaj sağlar
    • Serileştirmek veya incremental compilation framework’lerine bağlamak daha kolaydır; bu da hesaplama stratejisini esnekleştirir
  • Ancak küçük ölçekli programlamada indeks yaklaşımının kendisi zahmetli hâle gelir; hobi deneylerinde bu yük kritik olabilir
  • OCaml’de eski bir his hâlâ var ve bu bağlamda TypeScript’in ML’e karşılık gelen bir alternatif gibi kullanılıp kullanılamayacağı değerlendiriliyor

Deno ve TypeScript’in sunduğu hızlı deney ortamı

  • Başlangıç ortamı olarak deno kullanılıyor
    • TypeScript’i doğrudan kullanmaya imkân veren hemen kullanılabilir bir deneyim sunuyor
    • OCaml’de bu nokta sancılı; Rust, OCaml veya C++’tan daha iyi olsa da Deno, Rust’tan daha basit bir deneyim veriyor
  • Deno’nun geliştirme deneyimi küçük PLT hack’lerine iyi uyuyor
    • Tekil binary’dir
    • Linting ve formatting yerleşiktir
    • Ayrı bir derleme adımı yoktur
    • Yerleşik task runner ve watch modu vardır
  • TypeScript’in kendisi, yeterince esnek ve sözdizimsel yükü hafif bir tip sistemi sunar

Küçük bir typechecker örneğinde TypeScript kalıpları

  • AST, dosya konumu bilgisine sahip ifadelerle başlar
    • Location, file, line, column alanlarına sahiptir
    • TypeScript’te string yalnızca string, sayı da yalnızca number olduğundan usize ile u32 gibi ayrımları düşünmeye gerek kalmaz
  • İfadelerde konum ve kind ayrılır; daha sonra Expr<T> biçiminde ilişkili veri generic hâle getirilir
    • Parsing’in hemen ardından ifadeler void verisine sahiptir
    • Typechecker’ın işlediği ifadeler Type verisine sahiptir
    • Tip çıkarımı fonksiyonu ast.Expr<void> alıp ast.Expr<Type> döndürür
  • TypeScript runtime davranışını otomatik olarak eklemediği için union type’ları eşlemek adına runtime tip bilgisini doğrudan koymak gerekir
    • tag: "binary", tag: "if" gibi alanlar bu rolü üstlenir
    • tag: "binary", runtime’da ilgili değerin yalnızca "binary" string’i olabileceği anlamına gelir
  • Boolean literal ve int literal neredeyse aynı biçimde olduğundan ExprLiteral<T, V, Tag> ile soyutlanır
    • ExprBool<T>, ExprLiteral<T, boolean, "bool"> olur
    • ExprInt<T>, ExprLiteral<T, number, "int"> olur
  • Tip değerleri TypeBool ve TypeInt olarak ayrılır; singleton değerler de aynı adla sunulur
    • TypeScript tipleri tamamen sildiği için tipe ilişkin adlar ile değere ilişkin adlar ayrı namespace’lerde bulunur
    • Bu özellikten yararlanarak tip ve değer aynı adla tanımlanabilir

Visitor, transform, hata tipi ve desugaring

  • TypeScript’te switch bir ifade değil statement olduğu için, ifade kind’larını rahat işlemek adına visitor tanımlanır
    • bool, int, binary, if metotları ilgili kind’ı işler
    • Editör otomatik tamamlaması switch case’lerinde ve visitor uygulamasında yardımcı olur
  • transform<U, V>, Expr<U>’yu Expr<V>’ye dönüştüren genelleştirilmiş bir traversal fonksiyonudur
    • Dönüşüm bottom-up çalışır
    • İç node’lar ziyaret edilirken alt ifadeler zaten dönüştürülmüş olduğundan visitor tipi Visitor<U, V> değil Visitor<V, V> olur
  • TypeScript’in dinamik tipli bir dil olmasından yararlanarak Object.keys ile daha genel bir traversal da yapılabilir
    • Bu durumda bile statik fonksiyon imzası korunabilir
    • Örnekte şart değil, ancak gerekirse dinamik yaklaşıma kaçma alanı vardır
  • Tip hataları side effect olarak bir array’de biriktirilmez; TypeError tipiyle temsil edilir
    • Type, TypeBool | TypeInt | TypeError olur
    • TypeError, tag: "Error", location, message alanlarına sahiptir
    • type_equal, taraflardan biri Error ise true döndürerek zincirleme başarısızlığı engeller
  • Nihai typechecker, binary ve if ifadelerini kontrol eder
    • Binary ifade, sol ve sağ operand tipleri farklıysa "binary expression operands have different types" hatasını döndürür
    • If ifadesi, koşul boolean değilse "if condition is not a boolean" hatasını döndürür
    • Then ve else branch tipleri farklıysa "if branches have different types" hatasını döndürür
  • Ortaya çıkan kod bir miktar typing gerektirir; ancak otomatik tamamlama bunu büyük ölçüde telafi eder, dille mücadele ediyormuş hissi azdır ve problemin biçimine doğal şekilde uyar
  • TypeScript’in küçük dil hack’leri için üretken olmasının nedenleri üç maddede özetlenir
    • Deno küçük, kapalı, güçlü ve etkili geliştirme workflow’una optimize edilmiş bir scripting runtime’ıdır
    • TypeScript araçları, IDE’yi kullanışlı ve üretken kılar; Deno sayesinde yapılandırma gerekmez
    • Dil hem runtime hem compile time tarafında güçlüdür; tiplerle oldukça incelikli ifadeler kurarken gerektiğinde dinamik yaklaşıma kaçabilir
  • Ek bir fikir olarak, çok sayıda syntactic sugar’ı tip güvenli biçimde desugar etmek de mümkündür
    • Expr ve ExprKind, ilişkili veri yerine tüm ExprKind üzerinde recursive olarak parametrik hâle getirilir
    • ExprKindCore, temel ifade kümesini temsil eder
    • ExprKindSugar, temel ifadeleri veya temel ifadelere desugar edilebilen ifadeleri içerir
    • desugar(expr: ExprSugar): ExprCore, syntactic sugar ifadelerini çekirdek ifadelere indirger
    • desugar_one(expr: ExprKindSugar<ExprCore>): ExprKindCore<ExprCore>, alt ifadeler zaten desugar edilmişken tek adımlık dönüşüm yapar

1 yorum

 
GN⁺ 2023-08-20
Hacker News görüşleri
  • TypeScript genel olarak harika bir dil; fonksiyonların özelliklere/metotlara sahip olabilen nesneler olması ise yeterince takdir edilmiyor.
    Bir fonksiyon dizisini komutlar gibi çalıştırıp daha sonra help gibi açıklamalar ekleyebilir ya da closure/kısmi uygulama ile durum ekleyebilirsiniz; böylece Command gibi sınıfları aceleyle tanımlamanıza gerek kalmaz.
    Nesne yönelimli programlamada şeylere fazla erken isim vermek pek çok çatışma yaratıyor; VideoCompressor#compress() gibi bir yapı yerine gerekli değerleri fonksiyona geçirmek daha doğal görünüyor.

    • Daha doğrusu bu TypeScript’ten çok bir JavaScript özelliği.
      Fonksiyon gibi davranan nesneleri destekleyen diğer diller https://en.wikipedia.org/wiki/Function_object adresinde derlenmiş.
    • Go’da fonksiyonlara da metot eklenebilir; hatta pratikte hemen her tipe eklenebilir.
      net/http’deki handler’larda olduğu gibi bir struct serve metodunu uygulayabilir ya da bir handler fonksiyonu kendi kendini çağırarak arayüzü karşılayabilir.
      Clojure’da fonksiyon var’larına metadata ekleyerek benzer şekilde ele alınabilir; Lisp olduğu için makrolarla neredeyse her şey mümkündür.
      Ayrıca core/async’in CSP kanalları çalıştırma ile iletişimi ayırarak callback/promise/async/await gibi fonksiyon rengi sorunlarından kaçınmayı sağlar; komutlar da sonuçlarını kanala gönderen üreticiler gibi davranabilir.
    • Java’nın tek metotlu arayüzleri ile de benzer bir etki elde edilebilir.
      Metot adı ne olursa olsun fonksiyon bağlamında kullanılabilir ve somut metot adına referans vermek gerekmez.
      Ancak bir fonksiyonun özellikler taşıyıp durumu içinde barındırmasını ve aynı argümanlarla çağrılsa bile davranışının değişebilmesini tercih etmiyorum. Fonksiyonel programlamanın büyük avantajının, nesne yönelimli tarzdaki durumdan uzaklaşmak olduğunu düşünüyorum.
    • Python’da her şey nesne olduğu için bu yaklaşımı iyi destekler.
    • C#’ta da dönüş değeri olan fonksiyonlar Func, dönüş değeri olmayan fonksiyonlar Action ile benzer biçimde ifade edilebilir.
      JavaScript’in lambda ifadeleri C#’a oldukça benzer; TypeScript ve C#’ın fonksiyon imzaları da epey benzerdir.
      JavaScript, TypeScript ve C# arasındaki benzerlikleri gösteren küçük bir depo da var: https://github.com/CharlieDigital/js-ts-csharp
      Aynı mantığı JS/TS/C# olarak yan yana gösteren ekran görüntüsü: https://github.com/CharlieDigital/js-ts-csharp/blob/main/js-...
  • Çok şaşırtıcı değil. TypeScript de sonuçta ML ailesi özelliklerinin epey bir kısmını güç bela benimsemiş başka bir dil gibi görünüyor.
    Gerçek pattern matching olmadığı için OCaml’dan daha zahmetli, ama C#, Swift, Dart, Kotlin gibi dillerle karşılaştırıldığında fena sayılmaz.

    • Bu karşılaştırma fazla yüksek seviyeden yapılmış. Gerçek kullanım hissi oldukça farklı.
      TypeScript’in tip sistemi güçlü, ama temel standart kütüphanesi ve dilin kendisi yetersiz kalıyor; pattern matching/switch expression yok.
      Dart’ta nesne modeli kapalı olduğu için dinamik özgürlük daha düşük; tip sistemi ve ifadeleri de daha zayıf, metaprogramlama olanakları neredeyse yok; bu yüzden Java tarzı boilerplate’e ve kod üreteçlerine bel bağlıyor.
      C#, adı geçen diller arasında ML özelliklerine en yakın olanı; fakat TypeScript’in aksine sum type’ları olmadığı için pek çok iş daha zahmetli hale geliyor.
    • Eskiden Haskell ile yaptığım Pascal-C tarzı eğitim amaçlı bir derleyicide parser combinator’lar, sözdizimini BNF’ye benzer şekilde doğrudan kodla ifade etmeyi sağlıyordu.
      Örneğin { ... } yapısını tanıyan bir parser’ı başka parser’larla birleştirebilir, bir ifadeyi de kontrol akışı/deklarasyon/atama seçeneklerinden biri olarak tanımlayabilirsiniz.
      ML tarzı liste işleme ve pattern matching, ara temsilleri ele alırken çok ifade gücü yüksek oluyordu.
    • TypeScript’te pattern matching gerektiğinde şu kütüphaneyi severek kullanıyorum: https://github.com/gvergnaud/ts-pattern
    • TypeScript’in ML özelliklerinin çoğunu aldığı söylenemez.
      Metinde de switch bir ifade olmadığı için visitor pattern ile dolanmak gerekmişti; JavaScript’in iterator desteği de garip biçimde yetersiz.
      .map() var, ama yalnızca dizilerde çalışıyor; genel iterator’lara doğrudan uygulanamıyor.
    • TypeScript ile kotlin-js kullanıldığında iki dil birbirine epey yakın hissediliyor.
      Farkları çok olsa da geçiş zor değil; kişisel olarak Kotlin’i daha çok tercih etsem de ikisi de kullanılabilir.
      TypeScript, JavaScript uyumluluğundan çıkıp WASM’a derlense nasıl olur diye düşünüyorum. Kotlin bir WASM derleyicisi ekliyor ve zaten bir JS transpiler’ı da var; aynı kod WASM’de daha küçük oluyor ve daha hızlı yükleniyor.
      Tarayıcı JavaScript’i iyi bir derleme hedefi değil; yeni projelerin baştan TypeScript ile başlaması arttıkça, mevcut JavaScript’ten kolay geçiş yapılması gerektiği gerekçesi de giderek zayıflıyor.
  • Rust ile TypeScript arasında gidip gelince tagged enum gibi özelliklerin yokluğu çok net hissediliyor.
    ADT enum önerisi durmuş gibi görünüyor; başka bir çaba var mı merak ediyorum: https://github.com/Jack-Works/proposal-enum/discussions/19

    • Benzer kullanım için discriminated union’ların gayet iyi çalıştığını düşünüyorum
  • TypeScript’in tip sistemi eğlenceli, ama derleyici derlenen bir dille yazılmış olsaydı ne kadar daha hızlı olurdu diye insan merak ediyor
    Elbette bunun için “iyi bir implementasyon” gibi büyük bir önkoşul var

    • swc ve esbuild iyi karşılaştırma noktaları değil. Hız artışının önemli bir kısmı, TypeScript’e özgü söz dizimini kaldırıp JavaScript üretmelerinden geliyor
      tsc yalnızca ilk çalıştırmada yavaş sayılır; incremental bayrağı ya da --transpile-only izleme modu kullanıldığında derleme süresi genelde 100 ms’nin altına iner ve SWC veya ESBuild ile hissedilir fark neredeyse kalmaz
    • TypeScript ekibinin bir yanıtı var: https://twitter.com/drosenwasser/status/1260723846534979584
      Orta ölçekli bir programın tip denetimi 20 saniye sürüyorsa bunun çoğu zaman JS yüzünden değil, tiplerin kombinatoryal patlama yaratmasından kaynaklandığını söylüyor
      Başka çalışma ortamları paralellik veya başlangıç süresi açısından kazanç sağlayabilir, ama toplamda 20 kat hızlanmayı destekleyen CPU odaklı bir benchmark görmediklerini belirtiyor
    • Tip sistemi, generic’lerde sonuna kadar kullanana kadar eğlenceli
      Sonra 5 satırlık tip mantığını bütün gün debug edince insan kendine buraya nasıl geldiğini soruyor
    • Artık sadece hayal etmeye gerek yok: https://github.com/dudykr/stc
      SWC’nin ana geliştiricilerinden birinin Rust ile yazdığı bir proje; SWC TS’i JS’e derlerken STC TS tiplerini denetliyor
    • Son dönemde TypeScript derleyicisinin performansı ciddi ölçüde iyileşti
      Yaklaşan isolated declaration mode’un derleme süresini %75’e kadar azaltabileceği söyleniyor: https://github.com/microsoft/TypeScript/pull/53463#issuecomm...
  • Derleyicileri yeni öğrenmeye başlayanlara şu kitabı öneririm: https://keleshev.com/compiling-to-assembly-from-scratch/
    Yazar, TypeScript’in bir alt kümesiyle 32 bit ARM assembly derleyicisi yapıyor; neredeyse sözde kod gibi göründüğü için erişilebilir olduğunu açıklıyor

    • Crafting Interpreters’ı da şiddetle öneririm: https://craftinginterpreters.com/
      Kitap iki bölüme ayrılıyor; ilkinde Java ile bir dil yorumlayıcısı yapılıyor, ikincisinde aynı dil bytecode’a derlenip C ile bir bytecode sanal makinesi implemente ediliyor
      Implementasyondaki her kod satırına kitapta referans veriliyor
  • Visitor pattern’dan kaçınmak için run yardımcı fonksiyonuyla IIFE tarzı switch kullanılabilir
    Hemen çağrılan bir fonksiyonun içinde switch kullanıp dönüş tipinin çıkarılmasını sağlamak yeterli

    • Bu pattern hakkında bir yazı yazmıştım: https://maxgreenwald.me/blog/do-more-with-run
    • Aslında buna bile gerek yok; fonksiyonu hemen çağırınca dönüş tipi çıkarılıyor
      Sondaki () yüzünden IIFE’den kaçınmak istiyorsanız fonksiyonu ayrı tanımlayıp sonra çağırabilirsiniz
  • TypeScript ile bir derleyici yazıyorum ve sanıldığı kadar kötü olmadığına katılıyorum
    Başta yazar gibi Deno ile başladım ama sonunda Bun’a geçtim; pürüzlü yerleri olsa da Deno’dan daha memnunum ve çok hızlı
    Standart parser generator frontend’i olarak Ohm-js oldukça rahat: https://ohmjs.org/
    Resmî tsc derleyicisi okunamayacak kadar büyük, önermem; tsc’nin nasıl çalıştığını görmek için mini-typescript daha iyi: https://github.com/sandersn/mini-typescript/
    Özellikle centi-typescript branch’i yardımcı oluyor: https://github.com/sandersn/mini-typescript/tree/centi-types...
    WASM’de GC ve DOM erişiminin mümkün hâle gelmesini bekliyorum

    • Bun’a geçmenize neyin sebep olduğunu merak ediyorum
  • TypeScript’in interface’ler yüzünden ek yükünün daha fazla olacağını sanıyordum, şaşırtıcı
    Başka alanlara da uygulanabilir mi, örneğin dil ayrıştırma için de iyi olur mu merak ediyorum

    • Runtime’da interface’lerin yükü 0
      Derleme sürecinde tamamen ortadan kalkıyorlar
  • Sonuç çok şaşırtıcı değil. Çünkü TypeScript derleyicisinin kendisi TypeScript ile yazılmış durumda
    ML olarak TypeScript zaten her gün ağır production ortamlarında kendini kanıtlıyor

  • C# ile derleyici yazmıştım; burada özel görünen şey en fazla union type
    Kişisel olarak visitor pattern’ın laf kalabalığından kaçınmaya karar verdim ve derleme zamanında tamlık denetimi için kapalı enum özelliğini bekliyorum

    • Union type ya da Rust tarzı enum olmayan bir dil kullanınca bu özelliği çok özlüyorsunuz
      Alternatifler genelde garip oluyor. Bir sum type’ı temsil eden sınıfta nullable N adet property tutup “her zaman yalnızca biri non-null” şeklindeki dokümantasyondaki koşula güvenmek ya da birden fazla sınıfı ortak bir sınıftan türetmek; ikisi de ağır hissettiriyor
      Örtüşen birden fazla union type yapmak için iki yöntem de tekrar ya da akıllıca kombinasyonlar gerektiriyor