- 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
Hacker News yorumları
İş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.
Yeni keşfedenler, dilimlenmiş ekmekten beri en iyi icat olduğunu söyleyip susamazlar :)
Java için https://github.com/functionaljava/functionaljava var; desteği durmuş olsa da stabil.
O kadar güçlü ki onsuz olmaz.
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.
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
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.
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ç.
Makro kullanımı açısından o kadar karmaşık sayılmaz.
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://github.com/melt-umn/ableC-template-algebraic-data-ty...
Yine de geri kalanı çok güzel.
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.
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
O pratik ortadan kalktı ve bugün fonksiyon içinde kullanılan goto oldukça zararsız; pratikte break/continue ile neredeyse aynı
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'ıninline 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
https://gist.github.com/unclechu/eb37cc81e80afbbb5e74990b62e...
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
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
Pattern matching büyük bir kullanılabilirlik avantajıdır
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
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