- Datatype99 adalah library berbasis makro yang menyediakan tipe data aljabar, pattern matching yang exhaustive, dan introspeksi saat kompilasi di C99 murni
- Library ini bertujuan menangkap variant yang salah pengetikan tipenya, pattern matching yang tidak lengkap, dan akses field yang keliru pada waktu kompilasi, serta hanya memerlukan compiler C99 yang patuh standar
- Secara internal,
datatype diperluas menjadi tagged union dan value constructor, sedangkan match diperluas menjadi pernyataan switch, dan layout data yang dihasilkan mengikuti semantik generasi kode yang diformalkan
- Instalasi dilakukan dengan menambahkan
datatype99.h dan dependensi Metalang99 ke jalur include, dan pada GCC/Clang disarankan memakai opsi kompilasi untuk mengurangi keluaran error ekspansi makro
- Dapat diintegrasikan ke codebase C dengan
#include <datatype99.h>, diketahui berjalan di GCC, Clang, MSVC, dan TCC, serta juga mendukung C++11 ke atas
Apa yang disediakan Datatype99
- Datatype99 menyediakan tipe data aljabar yang aman dan intuitif di C99
- Cakupan fiturnya mencakup pattern matching yang exhaustive dan introspeksi saat kompilasi
- Berjalan tanpa alat generasi kode eksternal, dan implementasinya berbasis C99 murni
- Fitur utama
- Keamanan tipe: menangkap variant yang salah pengetikan tipenya, pattern matching yang non-exhaustive, dan akses field yang salah pada waktu kompilasi
- Portabilitas: memerlukan compiler C99 yang patuh standar, dan tidak membutuhkan library standar, fitur spesifik compiler/platform, maupun VLA
- Prediktabilitas: semantik generasi kode didefinisikan sehingga menjamin layout data yang dihasilkan selalu sama
- Error yang mudah dipahami: library itu sendiri mendeteksi sebagian error sintaks pada kode yang salah
- Kasus penggunaan nyata: dipakai di OpenIPC untuk pengembangan perangkat lunak streaming real-time bagi kamera IP, termasuk implementasi RTSP 1.0 dan sekitar 50 ribu baris kode privat
Instalasi dan konfigurasi build
- Datatype99 terdiri dari satu file header
datatype99.h dan dependensi Metalang99
- Untuk memakainya di proyek, tambahkan
datatype99 dan metalang99/include ke direktori include
- Di GCC, disarankan menetapkan
-ftrack-macro-expansion=0, dan di Clang -fmacro-backtrace-limit=1, untuk mengurangi keluaran error ekspansi makro yang tidak perlu
- Saat menggunakan CMake, direkomendasikan memakai
FetchContent
datatype99/CMakeLists.txt bawaan akan mengunduh Metalang99 v1.13.5 dari GitHub Releases
- Perilaku ini dapat dioverride dengan memanggil
FetchContent_Declare sebelumnya
- Header yang bergantung pada Datatype99 dapat dijadikan precompiled header, sehingga tidak perlu dikompilasi ulang setiap kali di-include dan waktu kompilasi bisa dipangkas
Cara penggunaan: memakai tagged union dengan lebih aman
- Pada dasarnya, Datatype99 adalah syntactic sugar untuk tagged union, yang memberikan bentuk yang lebih aman dan ringkas
- Dalam C biasa, untuk merepresentasikan pohon biner Anda harus menulis sendiri enum
tag dan union
- Di Datatype99, struktur yang sama bisa dideklarasikan seperti berikut
datatype(
BinaryTree,
(Leaf, int),
(Node, BinaryTree *, int, BinaryTree *)
);
- Dalam pendekatan
switch biasa, compiler mungkin tidak memberi peringatan walau Anda keliru mengakses tree->data.node setelah case Leaf:
- Dengan
match dan of, binding per-variant hanya terlihat di cabang tersebut, dan akses yang tidak semestinya akan membuat kompilasi gagal
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;
}
- Binding yang disediakan
of berupa variabel seperti x, lhs, rhs, dan bertipe pointer sehingga nilainya bisa diubah
- Untuk membuat variant digunakan value constructor yang dihasilkan secara internal
BinaryTree leaf5 = Leaf(5);
BinaryTree leaf7 = Leaf(7);
BinaryTree node = Node(&leaf5, 123, &leaf7);
Sintaks dan semantik generasi
- Datatype99 menyediakan sintaks makro seperti
datatype, record, match, of, otherwise, MATCHES, ifLet, dan lainnya
- Nama makro singkat dan versi ber-postfix tersedia bersamaan
- Contoh:
match99, of99, derive99
- Untuk menghindari konflik nama, Anda dapat mendefinisikan
DATATYPE99_NO_ALIASES sebelum include datatype99.h
- Pada header library, disarankan memakai makro versi postfix
datatype menghasilkan elemen-elemen berikut
typedef forward
- struct untuk setiap variant yang tidak kosong
typedef untuk tipe field variant
typedef untuk sum type
- tagged union yang mencakup enum
tag dan union
- value constructor
inline static untuk tiap variant
- pemanggilan deriver yang ditentukan di
derive(...)
- Walau semua variant kosong, standar C tetap mengharuskan union memiliki setidaknya satu anggota, sehingga ditambahkan
char dummy;
record adalah struct dengan proses derivation yang didefinisikan, dan jika tidak punya field juga akan menghasilkan char dummy;
match membandingkan instance sum type dengan variant secara berurutan, menjalankan pernyataan pada cabang yang berhasil, lalu berpindah ke instruksi berikutnya
- Bentuk lengkap
match dan ifLet masing-masing diperluas menjadi satu pernyataan C
MATCHES memeriksa benar/salah apakah instance sum type termasuk variant tertentu
matches sudah deprecated dan disarankan menggunakan MATCHES
derive dan atribut pembantu
derive(...) digunakan untuk menghasilkan kode global bagi sum type atau record
- Deriver untuk sum type dipanggil dalam bentuk makro yang kompatibel dengan Metalang99, dan daftar variant dikirim sebagai tuple list
- Deriver untuk record juga dipanggil sebagai makro kompatibel Metalang99, dan daftar field dikirim sebagai tuple list berbentuk
(<type>, <field-name>)
- Derive helper attribute adalah argumen bernama yang diteruskan ke deriver
- Helper attribute menggunakan bentuk object-like macro
#define <variant-name>_<namespace>_<attribute-name> attr(/* attribute value */)
- Makro manipulasi atribut yang disediakan
DATATYPE99_attrIsPresent / DATATYPE99_ATTR_IS_PRESENT: memeriksa apakah atribut ada
DATATYPE99_attrValue / DATATYPE99_ATTR_VALUE: mengambil nilai atribut yang ada
DATATYPE99_assertAttrIsPresent: memunculkan fatal error jika atribut wajib tidak ada
Pola penggunaan yang perlu diperhatikan
- Jangan memakai
break/continue tingkat teratas di dalam pernyataan yang diberikan ke of dan ifLet
continue di dalam loop for/while internal tetap aman
- Alur kontrol tingkat teratas harus diganti dengan label
goto
- Jika ingin menetapkan array sebagai parameter variant, array tersebut harus dimasukkan ke
struct terpisah
- Binding yang diperkenalkan
of selalu mutable, jadi bila nilai yang diberikan ke match adalah const, berhati-hatilah agar tidak mengubahnya
- Agar definisi
datatype tetap rapi, Anda bisa memakai // clang-format off dan // clang-format on di Clang-Format
- Derive helper attribute harus selalu di-
#undef setelah definisi datatype terkait untuk menghindari polusi namespace
- Jika makna parameter variant tidak jelas hanya dari konteks, Anda dapat memberi nama yang lebih deskriptif melalui type alias atau struct terpisah
Error, IDE, kompatibilitas
- Sebagian error sintaks dideteksi oleh library itu sendiri
- bentuk yang bukan tuple seperti
Bar(int)
- comma yang hilang
- trailing comma yang dilarang
- Error lainnya akan muncul lewat diagnostik compiler biasa
- nama tipe yang tidak ada
match yang non-exhaustive
- binder berlebih pada
of
- argumen variant dengan tipe yang salah
- pengembalian tanpa melakukan dereferensi pada binding pointer
- Berdasarkan pengalaman, hampir 95% error dikatakan terlihat bermakna
- Jika error sulit dipahami, Anda bisa memeriksa kode hasil generasi dengan
-E, dan karena semantik generasi kode didefinisikan secara formal, biasanya tidak akan muncul kode yang tak terduga
- VS Code otomatis mengaktifkan saran untuk tipe yang dihasilkan, tetapi tidak mendukung highlighting sintaks makro
- Datatype99 diketahui berjalan di GCC, Clang, MSVC, dan TCC
- C++11 ke atas juga didukung
Alasan menargetkan C dan keterbatasannya
- Perangkat lunak lama yang ditulis dalam C murni dapat memperoleh manfaat dari Datatype99
- Pada codebase C yang sudah ada, integrasi cukup dengan
#include <datatype99.h>
- Di beberapa lingkungan, C murni tetap dipertahankan karena alasan historis, seperti pada perangkat embedded, Linux, dan sistem operasi lain
- ABI C yang stabil penting bagi proyek sistem plugin seperti MetaCall
- C diposisikan sebagai bahasa yang matang dengan spesifikasi lengkap dan banyak library
- Jika Anda bisa memakai bahasa yang lebih modern atau lebih high-level, disarankan memakai bahasa tersebut alih-alih C lama, tetapi bagi banyak orang pilihan itu tidak tersedia atau biayanya besar
- Perbedaan Datatype99 dan Metalang99 terletak pada perannya
- Metalang99 adalah bahasa fungsional untuk metaprogramming
- Datatype99 adalah implementasi tipe data aljabar yang ditulis dengan Metalang99
1 komentar
Komentar Hacker News
Di pekerjaan saya harus memakai Java, dan sekarang saya merasa Java tidak seburuk yang dulu sering saya kritik, tetapi ada puluhan momen ketika saya berpikir, “andai Java punya discriminated union seperti F#”
Memang bisa ditiru dengan berbagai teknik, dan kadang enum saja sudah cukup, tetapi kebanyakan pendekatan itu tidak punya fleksibilitas dan keringkasan dari algebraic data type yang sesungguhnya. Yang paling utama, tidak ada pattern matching keren yang biasa didapat di bahasa fungsional
Ekstensi C ini terlihat punya pattern matching yang saya inginkan, jadi tampak cukup bagus, dan saya harus lihat apakah ini bisa dipakai untuk proyek Arduino
Orang yang baru menemukannya biasanya tidak bisa berhenti bicara seolah ini penemuan terbaik sejak roti :)
Untuk Java ada https://github.com/functionaljava/functionaljava, dan meski dukungannya sudah dihentikan, library itu stabil
Itu terlalu kuat untuk tidak dimiliki
Baru-baru ini sudah ada dukungan pattern matching yang dipaksakan compiler di atas sealed class, jadi rasanya sudah setengah jalan
Jika ingin melihat bagaimana algebraic data type bekerja di balik layar, menurut saya libsum adalah bahan pengantar yang bagus
[1] https://github.com/naasking/libsum
Saya sudah mengenal C hampir 20 tahun, tetapi tidak pernah menyangka sistem makro cukup kuat untuk memungkinkan sihir hitam seperti ini
Benar-benar keren
Secara tradisional ini dipakai untuk membuat dan mengakses generic yang type-safe, atau untuk mengurangi kode berulang pada definisi register perangkat keras dan interupsi
Rasanya memang agak terkutuk, tetapi intinya sebenarnya sangat sederhana, dan ini adalah alat yang andal untuk mengurangi kompleksitas kognitif dan boilerplate dalam proyek berbasis C
Untuk ukuran penggunaan makro, ini tidak terlalu rumit
https://en.wikipedia.org/wiki/Tagged_union#Class_hierarchies...
Ada juga tulisan blog panjang yang membahas hal yang sama, tetapi penulisnya tampaknya belum melihat bagian Wikipedia itu
https://nandakumar.org/blog/2023/12/paradigms-in-disguise.ht...
Patut dipuji bahwa pengembang datatype99 langsung menunjukkan masalah dari solusi tambal sulam seperti ini di README
https://github.com/melt-umn/ableC-template-algebraic-data-ty...
Meski begitu, selain itu semuanya sangat keren
Ini mengingatkan saya pada UI mode instan yang dulu pernah saya buat dengan makro. Dalam kasus saya itu bukan masalah, tetapi di beberapa blok, "break" memang bisa dipakai
Misalnya, di blok win_form kita bisa keluar dengan "break" dan "goto" juga berfungsi. Sebaliknya, blok win_command tidak menangkap "break", jadi jika memakai break atau goto di dalam win_command, itu akan keluar dari blok luar yang membungkus win_command tersebut, mungkin blok win_form. Biasanya ini dipakai untuk hal seperti tombol "Cancel"
Bukan sekadar sedikit lebih baik dalam beberapa hal, melainkan menghaluskan banyak ketidaknyamanan kecil yang menumpuk seiring waktu di seluruh ekosistem. Efek kumulatifnya besar, seperti masalah terlalu banyak tipe leksikal bawaan di keluarga CPython, atau seperti yang terjadi di library komputasi performa tinggi
Misalnya, bagian dari makro ini yang menimbulkan masalah seperti ini kemungkinan sejak awal tidak akan jadi masalah jika ditulis sebagai makro Rust yang baik. Ini adalah produk sampingan dari orang-orang pintar yang berusaha mengakali keterbatasan C
Namun, makro ini sendiri pada dasarnya adalah port dari fitur native Rust, jadi di Rust kemungkinan besar ini memang sejak awal tidak perlu ditulis, dan karena itu bisa dimanfaatkan secara alami dalam perangkat lunak komunitas
gotohanya jadi jebakan saat dipakai untuk berpindah dari satu fungsi ke fungsi lain, dan sebenarnya “goto considered harmful” juga menyorot praktik ituPraktik seperti itu sudah hilang, dan sekarang goto di dalam fungsi sendiri cukup tidak berbahaya, pada praktiknya hampir sama dengan break/continue
Dan misalkan Anda juga sudah tahu bahwa Rust itu ada, tetapi karena alasan yang sudah dipahami semua orang yang menulis program C, Anda tidak akan memakai Rust
Meski begitu, setidaknya Zig layak dipertimbangkan. Dua hari lalu saya menulis kode seperti ini dalam Zig
Dengan
inline elsemilikcomptime, saya menghasilkan semua cabang dari pernyataan switch di atas tagged union, lalu menambahkan offset ke anggota union yang memiliki field"l". Jika informasinya adalah informasi tipe yang bisa diketahui saat kompilasi, sifat cabangnya bisa diubah dengan berbagai cara, dan informasi seperti itu cukup banyak. Karena semua kondisi diputuskan saat kompilasi, setiap cabang switch hanya berisi logika yang diperlukan untuk menangani varian tersebut“Tapi program saya sudah C dan saya hanya membutuhkannya di satu berkas,” kalau begitu pun tetap ada baiknya mencoba Zig. Mungkin Anda akan menyukainya
https://gist.github.com/unclechu/eb37cc81e80afbbb5e74990b62e...
std::variantjuga tampaknya sampai taraf tertentu bekerja seperti sum typeSatu-satunya masalah adalah kita tidak bisa membuat switch yang rapi berdasarkan nilai tertentu dari field, tetapi switch bertingkat juga tidak terlalu berantakan
Masalah pendekatan ini adalah beban mental untuk menjaga semua detail itu tetap lengkap sangat besar. Karena lelah, kita jadi mulai memaksakan fitur ke kelas yang sudah ada alih-alih membuat kelas baru
Pada akhirnya abstraksi terasa mahal, dan akibatnya kita jadi lebih jarang menggunakannya meskipun sebenarnya abstraksi bisa menyelesaikan masalah dengan lebih elegan
Singkatnya, kemampuan untuk mengatakan “Darmok and Jalad at Tanagra” alih-alih harus mengulang cerita panjang setiap kali ingin merujuk pada gagasan yang rumit itu bersifat transformatif
Pattern matching adalah keuntungan kegunaan yang besar
Ini sulit dilakukan dengan makro, tetapi bukan tidak mungkin. Pada dasarnya dibutuhkan makro yang memulai konteks matching, memperkenalkan variabel untuk melacak semua alternatif yang sudah diperiksa, lalu di akhir konteks matching memeriksa apakah semua alternatif memang sudah dicek
std::variantCara mencocokkan semua kasus tanpa ada yang terlewat dengan
std::visitterasa agak hacky. Kalau ini benar-benar menjadi fitur bahasa kelas satu, manfaatnya akan besarDampaknya pada kode sehari-hari bisa lebih besar daripada fitur-fitur lain yang selama ini sedang dikerjakan, seperti coroutine