1 puan yazan GN⁺ 2024-05-10 | 1 yorum | WhatsApp'ta paylaş
  • Datatype99, saf C99 içinde cebirsel veri tipleri, exhaustive pattern matching ve derleme zamanı içgözlem sağlayan makro tabanlı bir kütüphanedir
  • Hatalı türlendirilmiş variant'ları, eksik pattern matching durumlarını ve yanlış alan erişimlerini derleme zamanında yakalamayı hedefler; yalnızca standarda uygun bir C99 derleyicisi gerekir
  • İçeride datatype, tagged union ve değer kurucularına (value constructor) genişler; match ise switch ifadesine genişler ve üretilen veri yerleşimi biçimselleştirilmiş kod üretim semantiğini izler
  • Kurulum, datatype99.h ile bağımlılığı olan Metalang99'u include yoluna ekleyerek yapılır; GCC/Clang'da makro genişletme hata çıktısını azaltan derleme seçeneklerinin kullanılması önerilir
  • #include <datatype99.h> ile C kod tabanına entegre edilebilir; GCC·Clang·MSVC·TCC üzerinde çalıştığı bilinir ve C++11 ve üzerini de destekler

Datatype99 neler sunuyor

  • Datatype99, C99 içinde güvenli ve sezgisel cebirsel veri tipleri sağlar
  • Özellik kapsamı exhaustive pattern matching ve derleme zamanı içgözlem yeteneklerini de içerir
  • Harici kod üretim araçları olmadan çalışır ve uygulama saf C99 tabanlıdır
  • Başlıca özellikler
    • Tür güvenliği: Hatalı türlendirilmiş variant'ları, exhaustive olmayan pattern matching durumlarını ve yanlış alan erişimlerini derleme zamanında yakalar
    • Taşınabilirlik: Standarda uygun bir C99 derleyicisi gerekir; standart kütüphane, derleyici/platforma özgü özellikler veya VLA gerekmez
    • Öngörülebilirlik: Kod üretim semantiği tanımlandığı için üretilen veri yerleşiminin her zaman aynı olması garanti edilir
    • Anlaşılır hatalar: Hatalı kod için kütüphane bazı sözdizimi hatalarını kendisi algılar
    • Gerçek kullanım örnekleri: OpenIPC, IP kameralar için gerçek zamanlı akış yazılımı geliştirmede bunu kullanır; buna RTSP 1.0 implementation ve yaklaşık 50 bin satırlık özel kod dahildir

Kurulum ve derleme ayarları

  • Datatype99, tek bir datatype99.h başlık dosyası ve Metalang99 bağımlılığından oluşur
  • Projede kullanmak için datatype99 ve metalang99/include dizinlerini include dizinlerine eklemek gerekir
  • GCC'de -ftrack-macro-expansion=0, Clang'da -fmacro-backtrace-limit=1 verilerek gereksiz makro genişletme hata çıktısının azaltılması önerilir
  • CMake kullanıldığında FetchContent önerilir
    • Varsayılan datatype99/CMakeLists.txt, GitHub Releases üzerinden Metalang99 v1.13.5 sürümünü indirir
    • Bu davranış, önceden FetchContent_Declare çağrılarak yeniden tanımlanabilir
  • Datatype99'a bağımlı başlıklar precompiled header olarak oluşturulabilir; böylece her include edildiğinde yeniden derlenmeyip derleme süresi azaltılabilir

Kullanım biçimi: tagged union'ı daha güvenli kullanmak

  • Datatype99 temelde tagged union için bir syntactic sugar sağlar ve bunu daha güvenli, daha kısa bir biçimde sunar
  • Normal C'de ikili ağaç ifade etmek için tag enum'u ile union'ı elle yazmak gerekir
  • Datatype99'da aynı yapı şu şekilde tanımlanabilir
datatype(
    BinaryTree,
    (Leaf, int),
    (Node, BinaryTree *, int, BinaryTree *)
);
  • Normal switch yaklaşımında, case Leaf: sonrasında yanlışlıkla tree->data.node alanına erişseniz bile derleyici uyarı vermeyebilir
  • match ve of kullanıldığında, variant'a özel bağlamalar yalnızca ilgili dal içinde görünür olur ve uygunsuz erişim derlemenin başarısız olmasına yol açar
int sum(const BinaryTree *tree) {
    match(*tree) {
        of(Leaf, x) return *x;
        of(Node, lhs, x, rhs) return sum(*lhs) + * x + sum(*rhs);
    }

    return -1;
}
  • of tarafından sağlanan bağlamalar x, lhs, rhs gibi değişkenlerdir; bunlar pointer tipindedir ve değerlerin değiştirilmesine olanak tanır
  • Variant oluşturmak için içeride üretilen değer kurucuları kullanılır
BinaryTree leaf5 = Leaf(5);
BinaryTree leaf7 = Leaf(7);
BinaryTree node = Node(&leaf5, 123, &leaf7);

Sözdizimi ve üretim semantiği

  • Datatype99; datatype, record, match, of, otherwise, MATCHES, ifLet gibi makro sözdizimleri sunar
  • Kısaltılmış makro adları ve sonekli sürümler birlikte bulunur
    • Örnek: match99, of99, derive99
    • İsim çakışmalarını önlemek için datatype99.h include edilmeden önce DATATYPE99_NO_ALIASES tanımlanabilir
    • Kütüphane başlıklarında sonekli makroların kullanılması önerilir
  • datatype şu öğeleri üretir
    • forward typedef
    • boş olmayan her variant için struct
    • variant alan tipleri için typedef
    • sum type için typedef
    • tag enum'u ve union içeren bir tagged union
    • her variant için inline static değer kurucuları
    • derive(...) içinde belirtilen deriver çağrıları
  • Tüm variant'lar boş olsa bile, C standardı gereği union içinde en az bir öğe bulunması gerektiğinden char dummy; eklenir
  • record, türetme süreci tanımlanmış bir structtır; alan olmadığında da char dummy; üretilir
  • match, bir sum type örneğini variant'larla sırayla karşılaştırır, eşleşen dalın ifadelerini çalıştırır ve ardından bir sonraki komuta geçer
  • Tam match ve ifLet sözdizimleri ayrı ayrı tek bir C ifadesine genişler
  • MATCHES, bir sum type örneğinin belirli bir variant'a ait olup olmadığını doğru/yanlış olarak sınar
  • matches deprecated durumdadır; MATCHES kullanılması önerilir

derive ve yardımcı öznitelikler

  • derive(...), bir sum type veya record için global kod üretmek amacıyla kullanılır
  • Sum type deriver'ı, Metalang99 uyumlu makro biçiminde çağrılır; variant listesi tuple list olarak iletilir
  • Record deriver'ı da Metalang99 uyumlu makro olarak çağrılır; alan listesi (<type>, <field-name>) biçiminde tuple list olarak iletilir
  • Derive helper attribute, deriver'a geçirilen isimli argümanlardır
  • Helper attribute, object-like macro biçimini kullanır
#define <variant-name>_<namespace>_<attribute-name> attr(/* attribute value */)
  • Sağlanan öznitelik işleme makroları
    • DATATYPE99_attrIsPresent / DATATYPE99_ATTR_IS_PRESENT: özniteliğin var olup olmadığını kontrol eder
    • DATATYPE99_attrValue / DATATYPE99_ATTR_VALUE: var olan özniteliğin değerini çıkarır
    • DATATYPE99_assertAttrIsPresent: gerekli öznitelik yoksa fatal error üretir

Dikkat edilmesi gereken kullanım kalıpları

  • of ve ifLet içine verilen ifadelerde en üst düzey break/continue kullanılmamalıdır
    • İçteki for/while döngülerindeki continue kullanılabilir
    • En üst düzey kontrol akışı goto etiketiyle değiştirilmelidir
  • Variant parametresi olarak dizi belirtmek için ayrı bir struct içine alınmalıdır
  • of tarafından tanıtılan bağlamalar her zaman mutable olduğundan, match içine verilen değer const ise değiştirmemeye dikkat edilmelidir
  • datatype tanımını okunaklı tutmak için Clang-Format ile // clang-format off ve // clang-format on kullanılabilir
  • Namespace kirliliğini önlemek için derive helper attribute ilgili datatype tanımının ardından her zaman #undef edilmelidir
  • Variant parametrelerinin anlamı yalnızca bağlamdan açık değilse, tür takma adları veya ayrı struct'larla daha açıklayıcı adlar verilebilir

Hatalar, IDE, uyumluluk

  • Bazı sözdizimi hataları doğrudan kütüphane tarafından algılanır
    • Bar(int) gibi tuple olmayan biçimler
    • eksik comma
    • yasak trailing comma
  • Diğer hatalar da normal derleyici tanılarıyla ortaya çıkar
    • var olmayan tür adı
    • exhaustive olmayan match
    • of içinde aşırı sayıda binder
    • hatalı türlendirilmiş variant argümanları
    • pointer binding'i dereference etmeden yapılan dönüş
  • Deneyime göre hataların yaklaşık %95'i anlamlı şekilde görünür
  • Hata anlaşılmıyorsa -E ile üretilen kod incelenebilir; kod üretim semantiği biçimsel olarak tanımlandığı için genelde beklenmedik bir kod oluşmaz
  • VS Code, üretilen tipler için önerileri otomatik etkinleştirir ancak makro sözdizimi vurgulamasını desteklemez
  • Datatype99'un GCC, Clang, MSVC ve TCC üzerinde çalıştığı bilinir
  • C++11 ve üzeri de desteklenir

Neden C hedefleniyor ve sınırlamalar

  • Saf C ile yazılmış mevcut yazılımlar Datatype99'un avantajlarından yararlanabilir
  • Mevcut C kod tabanına yalnızca #include <datatype99.h> ile entegre edilebilir
  • Bazı ortamlarda gömülü cihazlar, Linux ve diğer işletim sistemlerinde tarihsel nedenlerle saf C korunur
  • C'nin kararlı ABI'si, MetaCall gibi eklenti sistemi projeleri için önemlidir
  • C, tam bir spesifikasyona ve çok sayıda kütüphaneye sahip olgun bir dil olarak sunulur
  • Daha modern veya daha yüksek seviyeli bir dil kullanmak mümkünse, eski C yerine böyle dillerin kullanılması önerilir; ancak birçok kişi için bu seçenek ya yoktur ya da maliyeti yüksektir
  • Datatype99 ile Metalang99 arasındaki fark, rollerindedir
    • Metalang99, metaprogramming için işlevsel bir dildir
    • Datatype99, Metalang99 ile yazılmış bir cebirsel veri tipi uygulamasıdır

1 yorum

 
GN⁺ 2024-05-10
Hacker News yorumları
  • Emirsel diller kullandığım her seferinde cebirsel veri tiplerini neredeyse her zaman özlüyorum.
    İşte Java kullanmak zorundayım; Java'nın eskiden eleştirildiği kadar kötü olmadığını düşünmeye başladım ama “keşke Java'da F#'taki discriminated union olsaydı” dediğim onlarca an oldu.
    Çeşitli tekniklerle taklit edilebiliyor ve enum'ların tek başına yeterli olduğu çok durum var, ama çoğu zaman gerçek cebirsel veri tiplerinin esnekliği ve sadeliği eksik kalıyor. En önemlisi de fonksiyonel dillerde elde edilen harika pattern matching yok.
    Bu C uzantısı, istediğim pattern matching'e sahip gibi görünüyor; epey iyi duruyor, Arduino projelerinde kullanılıp kullanılamayacağına bakmam gerekecek.
    • Cebirsel veri tipleri ve pattern matching kullanmamış olanlar neyin bu kadar büyük mesele olduğunu anlamaz; alışık olanlar da bunların olmadığı bir dilde çalışana kadar neyin bu kadar büyük mesele olduğunu bilmez.
      Yeni keşfedenler, dilimlenmiş ekmekten beri en iyi icat olduğunu söyleyip susamazlar :)
    • Java 21'in sealed interface özelliğiyle pattern matching mümkün.
    • Kotlin, JVM ile uyumlu ve cebirsel veri tipleri var.
      Java için https://github.com/functionaljava/functionaljava var; desteği durmuş olsa da stabil.
    • Cebirsel veri tipleri ve pattern matching aslında emirsel dillerde de gayet iyi çalışır. Örneğin Rust var.
  • Bir ürünü baştan yeniden uygulayacak olsam, derleyicinin zorunlu kıldığı eksiksiz pattern matching sunan discriminated union'ları şart koşardım.
    O kadar güçlü ki onsuz olmaz.
    • Dart'ta en çok istediğim şey union tipleri, ama ne yazık ki yakın zamanda eklenecek gibi görünmüyor.
      Yakın zamanda sealed class'lar üzerinde derleyicinin zorunlu kıldığı pattern matching desteği geldi; yani yolun yarısına gelinmiş gibi görünüyor.
    • Şu anda bu tanıma uyan hangi diller var? Tam olarak ne demek istediğini pek bilmiyorum ama destekleyen dillerden örnekleri görürsem daha iyi anlayabilirim.
  • Kesinlikle daha iyi görünüyor ve muhtemelen geçmişte yaptığım denemeden daha iyi çalışacaktır [1], ama kodu 8 kat daha fazla ve mükemmel ama biraz ürkütücü Metalang9 makro araç setine bağımlı.
    Cebirsel veri tiplerinin içeride nasıl çalıştığını görmek istiyorsanız libsum'un iyi bir başlangıç materyali olduğunu düşünüyorum.
    [1] https://github.com/naasking/libsum
    • O depoya yıldız vermiş olduğumu görünce, Datatype99'u tasarlarken bakmış olmalıyım gibi geliyor :)
  • Bu bir büyücünün işi.
    C'yi neredeyse 20 yıldır biliyorum ama makro sisteminin böyle kara büyüleri mümkün kılacak kadar güçlü olduğunu hiç düşünmemiştim.
    Gerçekten harika.
    • Yazar sadece 19 yaşında. Bir anda kendimi aptal gibi hissettim.
    • Aynı yazarın yaptığı metalang99 da ilginizi çekebilir.
    • x-macro, yani makroların bu şekilde kullanılması, epey gösterişli.
      Geleneksel olarak tip güvenli generic'ler oluşturup bunlara erişmek ya da donanım register'ları ve interrupt tanımlarındaki tekrarlı kodu azaltmak için kullanılırdı.
      Biraz lanetli hissettirse de özü aslında çok basit ve C tabanlı projelerde bilişsel karmaşıklığı ve tekrarlı kodu azaltmak için güvenilir bir araç.
    • Cebirsel veri tipleri genel olarak generic struct ve union'lar üzerinde, union tag'i eklenmiş metin ikamesinden ibarettir.
      Makro kullanımı açısından o kadar karmaşık sayılmaz.
  • Wikipedia'da bununla ilgili ilginç bir bölüm var: union'ları “nesne yönelimli programlamadaki sınıf hiyerarşisi” olarak uygulama yöntemi.
    https://en.wikipedia.org/wiki/Tagged_union#Class_hierarchies...
    Aynı konuyu ele alan uzun bir blog yazısı da var; yazarın henüz ilgili Wikipedia bölümünü görmemiş olduğu anlaşılıyor.
    https://nandakumar.org/blog/2023/12/paradigms-in-disguise.ht...
    datatype99 geliştiricisinin bu tür geçici çözümlerin sorununu README'de doğrudan göstermesi takdire değer.
  • https://melt.cs.umn.edu/ de var. C'ye template ve cebirsel veri tipleri ekleyen bir uzantısı var.
    https://github.com/melt-umn/ableC-template-algebraic-data-ty...
  • “of ve ifLet'e verilen statement'ların içinde en üst seviyede break/continue kullanmayın; bunun yerine goto label kullanın” denmesi oldukça büyük bir ayağa sıkma unsuru gibi görünüyor.
    Yine de geri kalanı çok güzel.
    • goto kullanmanın kendisi sorun değil; sorun, böyle blokların içinde break/continue kullanmamanız gerektiğini bilmek zorunda olmanız.
      Bir zamanlar makrolarla immediate mode UI yapmıştım, bu onu hatırlattı. Benim durumumda sorun olmamıştı, ama bazı bloklarda "break" kullanılabiliyor.
      Örneğin win_form bloğunda "break" ile çıkabilirsiniz ve "goto" da çalışır. Buna karşılık win_command bloğu "break"i yakalamaz; bu yüzden win_command içinde break ya da goto kullanırsanız, o win_command'ı saran dış bloktan, muhtemelen win_form bloğundan çıkarsınız. Genellikle "Cancel" düğmesi gibi durumlarda kullanılır.
    • Rust'ın makro alanının iyi yanı, bu tür koşulları denetleyen kod yazmanın yalnızca mümkün değil, aynı zamanda pratik ve hayal edilebilir olması; ayrıca kolayca kurulabilen açık kaynak kütüphanelerden de yardım alabilmeniz.
      Bu sadece birkaç açıdan biraz daha iyi olmak değil; ekosistem genelinde zamanla biriken sayısız küçük pürüzü pürüzsüzce ortadan kaldırmakla ilgili. CPython çevresinde kendine özgü fazla sayıda lexical tip olması ya da yüksek performanslı hesaplama kütüphanelerindeki benzer sorunlar gibi, birikimli etkisi büyük.
      Örneğin bu makroda böyle bir soruna yol açan kısım, iyi yazılmış bir Rust makrosunda en başta sorun olmazdı. Bu, zeki insanların C'nin sınırlarını aşmaya çalışırken ortaya çıkardığı bir yan ürün.

Ancak bu makronun kendisi de zaten Rust'ın yerel bir özelliğinin taşınmış hâli olduğundan, Rust'ta en başta yazılmasına gerek kalmazdı; bu yüzden topluluk yazılımında doğal biçimde kullanılıyor

  • goto yalnızca bir fonksiyondan başka bir fonksiyona atlamak için kullanıldığında insanın ayağına kurşun sıkmasına yol açan bir unsur; aslında “goto considered harmful” da bu pratiği hedef alıyordu
    O pratik ortadan kalktı ve bugün fonksiyon içinde kullanılan goto oldukça zararsız; pratikte break/continue ile neredeyse aynı
  • Diyelim ki C programı yazmanız gerekiyor ve union tag'leri üzerinde eksiksiz pattern matching gerçekten gerekli. Datatype99'un sunduğu şey de “kısaca, tag'li union üzerinde sözdizimsel şeker”
    Ayrıca Rust'ın var olduğunu zaten biliyorsunuz ama C programı yazanların iyi bildiği nedenlerle Rust kullanmayacağınızı varsayalım
    Yine de en azından Zig'i değerlendirmeye değer. İki gün önce Zig ile böyle bir kod yazdım
    comptime'ın inline else özelliğini kullanarak tag'li union üzerindeki switch ifadesinin tüm dallarını üretiyor ve "l" alanı olan union üyelerine offset ekliyor. Derleme zamanında bilinebilen tip bilgileriyle dalların niteliğini çeşitli biçimlerde değiştirebilirsiniz; bu bilgi de epey fazla. Tüm koşullar derleme zamanında belirlendiği için switch'in her dalı yalnızca ilgili varyantı işlemek için gereken mantığa sahip olur
    “Ama programım zaten C ve buna yalnızca tek bir dosyada ihtiyacım var” diyorsanız bile, yine de Zig'i denemeniz iyi olabilir. Hoşunuza gidebilir
  • struct + union + enum ile cebirsel veri tiplerinin avantajlarının çoğunu elde edemez miyiz? Birden fazla tipten oluşan bir union kullanıp hangisinin seçileceğini ayırt eden bir enum ekleme desenini daha önce kullanmıştım
    std::variant da sum type gibi bir ölçüde çalışıyor gibi görünüyor
    Tek sorun, alanın belirli bir değerine göre temiz bir switch ifadesi kuramamak; ama iç içe switch ifadeleri de o kadar dağınık değil
    • Doğru. Yalnızca konvansiyon ve disiplinle de nesne yöneliminin birçok avantajını elde edebilirsiniz; ama bunu yapmak için örneğin vtable'ı elle yönetmeniz gerekir, dolayısıyla sık sık uygulama ayrıntılarına inersiniz
      Bu yaklaşımın sorunu, tüm ayrıntıları eksiksiz takip etmenin zihinsel yükünün büyük olması. İnsan yoruldukça yeni sınıf oluşturmak yerine işlevleri mevcut sınıflara zorla sıkıştırmaya başlar
      Sonunda soyutlama pahalıymış gibi gelir ve bunun sonucunda sorun daha zarif çözülebilecekken daha az soyutlama kullanılır
      Özetle, karmaşık bir fikre her atıf yaptığınızda uzun hikâyeyi yeniden anlatmak yerine “Darmok and Jalad at Tanagra” diyebilme yeteneği dönüştürücüdür
    • Modelleme tarafı taklit edilebilir. Ama bu, cebirsel veri tiplerinin avantajlarının yarısı bile değil
      Pattern matching büyük bir kullanılabilirlik avantajıdır
    • Sum type değerleriyle çalışırken tüm alternatifleri kontrol ettiğinizden emin olmak kesinlikle çok önemlidir
      Makroyla yapmak zor ama imkânsız değil. Temelde eşleme bağlamını başlatan bir makroya ihtiyacınız var; kontrol edilen tüm alternatifleri izleyecek bir değişken tanıtmalı ve eşleme bağlamı bittiğinde tüm alternatiflerin kontrol edilip edilmediğini denetleyecek şekilde kurgulamalısınız
    • Gerçekten gerektiğinde kodun çoğunda C++ kullanmak genel olarak fena değil, ama std::variant'ı pek sevmiyorum
      std::visit ile tüm durumları eksiksiz eşleme biçimi hacky geliyor. Gerçekten birinci sınıf bir dil özelliği olursa büyük kazanç sağlar
      Coroutine'ler gibi şimdiye kadar üzerinde çalışılan diğer özelliklerden günlük kod üzerinde daha büyük etkisi olabilir
  • Bunu hayata geçirmiş olmaları gerçekten harika. Saygı duyuyorum