Mengapa Efek Aljabar Diperlukan
(antelang.org)- Efek aljabar (effect handlers) adalah alat alur kontrol yang fleksibel yang memungkinkan berbagai fitur bahasa (penanganan pengecualian, generator, coroutine, dan lain-lain) diimplementasikan pada level library
- Ini juga dapat diterapkan pada manajemen konteks, injeksi dependensi, pengganti status global, dan lain-lain yang umum dalam pemrograman fungsional
- Berkontribusi pada kesederhanaan desain API dan otomatisasi penyampaian status/lingkungan di dalam kode
- Juga mendukung keunggulan seperti jaminan kemurnian fungsional, replayability, dan audit keamanan
- Dengan perkembangan terbaru dalam teknologi compiler, masalah performa juga telah banyak membaik
Ringkasan Efek Aljabar (Algebraic Effects)
Efek aljabar (juga dikenal sebagai effect handlers) adalah fitur bahasa pemrograman yang belakangan ini semakin mendapat sorotan. Sebagai salah satu fitur inti di Ante dan berbagai bahasa riset (Koka, Effekt, Eff, Flix, dan lain-lain), tren adopsinya berkembang pesat. Banyak materi menjelaskan konsep effect handlers, tetapi penjelasan mendalam tentang "mengapa" fitur ini dibutuhkan dalam praktik masih kurang. Artikel ini memperkenalkan kegunaan nyata dan manfaat efek aljabar seluas mungkin.
Memahami sintaks dan semantik dengan cepat
- Efek aljabar adalah konsep yang mirip dengan "pengecualian yang dapat dilanjutkan kembali"
- Fungsi efek dapat dideklarasikan seperti
effect SayMessage - Seperti
foo () can SayMessage = ..., fungsi dapat menyatakan kemungkinan penggunaan efek tersebut - Dengan
handle foo () | say_message () -> ..., efek dapat ditangani seperti try/catch pada pengecualian
Melalui struktur dasar ini, pemanggilan dan kontrol efek menjadi memungkinkan.
Memperluas alur kontrol kustom
Alasan terbesar keberadaan efek aljabar adalah bahwa dengan satu fitur bahasa ini saja, berbagai kemampuan yang semula memerlukan fitur bahasa terpisah masing-masing—seperti generator, pengecualian, coroutine, dan asinkron—dapat diimplementasikan sebagai library.
- Jika fungsi memiliki variabel efek polimorfik (
can e), berbagai efek dapat diteruskan dan digabungkan melalui argumen fungsi - Sebagai contoh, fungsi
mapdapat dideklarasikan agar fungsi yang diterimanya boleh menggunakan efek arbitrere, sehingga bisa digabungkan secara alami dengan berbagai efek (output, asinkron, dan lain-lain)
Contoh implementasi pengecualian dan generator
- Implementasi pengecualian: jika setelah memicu efek tidak ada pemanggilan
resume, perilakunya sama seperti pengecualian - Implementasi generator: dengan mendefinisikan efek
Yield, setiap kali nilai di-yield, handler luar dapat ikut campur untuk mengendalikan alur sesuai kondisi, dan pola tingkat lanjut seperti filtering pun dapat ditulis dengan kode yang relatif sederhana
Kemampuan menggabungkan beberapa efek sekaligus juga merupakan keunggulan besar dibanding teknik abstraksi efek yang sudah ada.
Pemanfaatan sebagai lapisan abstraksi
Efek aljabar bukan hanya berguna untuk memperluas fitur inti pemrograman, tetapi juga sangat bermanfaat dalam berbagai skenario aplikasi bisnis.
Injeksi dependensi (Dependency Injection)
- Objek dependensi seperti database dan output dapat diabstraksikan sebagai efek lalu dikelola melalui handler
- Penggantian dengan objek mock untuk pengujian, pengalihan output, dan sebagainya juga dapat diimplementasikan dengan fleksibel
Logging bersyarat atau manajemen output
- Berdasarkan level logging, apakah pesan log ditampilkan atau tidak dapat dikendalikan secara terpusat
Penyederhanaan desain API dan otomatisasi penyampaian Context
Pemanfaatan efek status (State)
- Dalam situasi yang memerlukan penyampaian objek context atau informasi lingkungan, jika diimplementasikan berbasis efek agar hanya memakai
get/set, manajemen status dapat diotomatisasi tanpa penyampaian eksplisit - Sebelumnya context harus diteruskan sebagai argumen ke semua fungsi, tetapi bagian ini dapat disembunyikan dengan state effect
Pengganti objek global
- Status yang sebelumnya dikelola sebagai objek global, seperti generator angka acak atau alokasi memori, juga dapat diabstraksikan sebagai effect, sehingga lebih baik dari sisi kejelasan kode, kemudahan pengujian, dan dukungan konkurensi
- Cukup dengan mengganti handler, sumber angka acak yang sebenarnya dapat diubah dengan fleksibel
Dukungan penulisan gaya langsung (Direct Style)
- Sebelumnya beberapa objek harus ditangani secara bertingkat dengan option type, error wrapping, dan sebagainya
- Efek memungkinkan jalur error atau efek samping diekspresikan dengan bersih tanpa pembungkusan seperti itu
Jaminan kemurnian dan audit keamanan
Penandaan efek samping secara eksplisit
- Di sebagian besar bahasa dengan effect handlers, fungsi yang menimbulkan efek samping wajib mencantumkan efek seperti
can IO,can Print, dan lain-lain dalam type signature - Pada pembuatan thread, software transactional memory (STM), dan sebagainya, fungsi murni wajib digunakan
Replay log dan jaringan deterministik
- Berdasarkan kemurnian, handler seperti
recorddanreplaydapat dibuat untuk mereproduksi hasil eksekusi - Ini memungkinkan hasil deterministik dan dukungan rollback untuk debugging, database, jaringan game, dan lain-lain
Dukungan Capability-based Security
- Semua efek yang belum ditangani terekspos pada type signature fungsi, sehingga efektif untuk audit keamanan library eksternal
- Jika sebuah fungsi yang sebelumnya tidak memiliki efek samping diperbarui hingga memiliki
can IO, hal itu dapat langsung terdeteksi di kode yang memanggilnya
Namun, karena semua efek otomatis dipropagasikan, efek juga bisa tertangani tanpa disadari sebagai konsekuensi samping.
Perspektif efisiensi dan kesimpulan
- Dulu kelemahan utamanya adalah efisiensi eksekusi, tetapi belakangan ini optimisasi telah berkembang pesat dalam banyak kasus, termasuk efek tail-resumptive
- Berbagai bahasa juga menerapkan strategi kompilasi yang efektif masing-masing (closure call, evidence passing, spesialisasi handler, dan lain-lain)
Efek aljabar diharapkan akan menempati posisi yang jauh lebih penting dalam bahasa pemrograman masa depan.
1 komentar
Komentar Hacker News
Menurut saya ada dua kekurangan.
Dari potongan kode yang diberikan, kekurangan pertama adalah sama sekali tidak ada penanda bahwa
fooataubarbisa gagal.Untuk mengetahui bahwa pemanggilan seperti ini dapat memicu error handler, kita harus mencari sendiri type signature-nya, dan tergantung situasinya, itu perlu dilakukan secara manual tanpa bantuan IDE.
Kekurangan kedua adalah, setelah mengetahui bahwa
foodanbarbisa gagal, untuk mencari kode apa yang benar-benar dijalankan saat kegagalan terjadi, kita harus menelusuri call stack cukup jauh ke atas untuk menemukan ekspresiwith, lalu setelah itu mengikuti handler tersebut ke bawah.Tidak mungkin menelusuri perilaku ini secara statis atau langsung lompat ke definisinya di IDE, karena
my_functionbisa dipanggil di banyak tempat dengan handler yang berbeda-beda.Saya pikir konsep ini sangat segar, tetapi pada akhirnya saya tetap punya kekhawatiran dari sisi keterbacaan kode maupun debugging.
Terkait masalah mencari kode apa yang berjalan saat eksekusi gagal, dijelaskan bahwa inilah inti dari dynamic code injection.
Sama seperti berbagai fitur dinamis lain seperti shallow-binding dan deep-binding, binding dilakukan dengan mengikuti call stack.
Ketidakmungkinan analisis statis atau lompatan IDE juga berasal dari sifat dinamis ini.
Namun saya rasa dalam proses ini sebenarnya tidak banyak yang perlu terlalu dipedulikan.
Karena ini adalah cara menambahkan efek ke kode murni, maka bergantung pada situasinya, baik efek murni maupun tidak murni bisa dihubungkan ke berbagai konteks seperti mock untuk pengujian atau environment produksi.
Prinsipnya mirip dengan dependency injection.
Ini juga bisa diimplementasikan secara mirip dalam monad tradisional, tetapi untuk menemukan di mana tepatnya monad diinstansiasi, kita tetap harus memeriksa call stack.
Teknik-teknik seperti ini memang punya manfaat, tetapi jelas ada harganya juga.
Menguntungkan untuk pengujian dan sandboxing, tetapi punya sifat bahwa apa yang sebenarnya terjadi di dalam kode tidak terlihat secara jelas.
Saya pernah menulis skripsi sarjana tentang dukungan IDE untuk lexical effects dan handler.
Saya rasa semua poin yang disorot di atas cukup mungkin untuk diwujudkan.
Tautan skripsi
Di ekosistem .NET ada kecenderungan memakai interface secara berlebihan, sehingga cukup merepotkan karena harus melewati beberapa langkah hanya untuk langsung melompat ke implementasi method.
Sering kali jika implementasinya berada di assembly lain, fitur IDE jadi tidak berguna.
Dalam Dependency Injection tingkat lanjut, khususnya Autofac, scope dibangun secara hierarkis seperti variabel dynamic scope di LISP untuk menentukan saat runtime service akan di-bind ke instance yang mana.
Dalam hal ini, effect dapat diinjeksikan sebagai instance interface seperti
ISomeEffectHandler, lalu saat effect terjadi, hal itu direpresentasikan sebagai pemanggilan method terkait.Perilaku konkret handler, seperti melempar exception atau melakukan logging, ditentukan secara dinamis sesuai konfigurasi DI.
Dulu pola yang dipakai adalah
throwexception, tetapi desainnya bisa diubah menjadi menyatakan effect secara eksplisit berbasis interface dan menyerahkan sepenuhnya cara penanganannya ke DI.Saya belum sempat menggali lebih dalam sampai ke hal-hal terkait iterator seperti
yield.Saya pikir inti poinnya justru ada pada tidak adanya penanda bahwa
foodanbarbisa gagal.Kita jadi bisa menulis kode dalam direct style tanpa memikirkan context efeknya.
Mencari kode apa yang berjalan saat gagal juga merupakan inti dari abstraksi.
Handler efek mana yang benar-benar akan terhubung saat runtime diputuskan belakangan.
Prinsipnya mirip seperti pada
f : g:(A -> B) -> t(A) -> B, di mana kita juga tidak bisa tahu sebelumnya kode apa yang akan dijalankan ketikagdieksekusi.Saya tidak setuju dengan klaim bahwa mustahil melakukan analisis statis untuk mencari handler dengan menelusuri call stack ke atas.
Sebenarnya analisis statis bisa dilakukan, dan di IDE kita bisa memakai fitur seperti "go to caller" untuk memilih handler mana yang digunakan.
"pseudocode" Ante sangat mengesankan.
Rasanya seperti perpaduan yang pas antara karakteristik Haskell dengan ekspresivitas dan kepraktisan Elixir.
Kesan saya, ini seperti Haskell untuk para developer.
Saya berharap kompiler-nya makin matang.
Saya ingin sekali mencoba membuat aplikasi dengan Ante.
Terkait klaim bahwa AE (Algebraic Effects) menggeneralisasi control flow sehingga coroutine juga bisa diimplementasikan dengannya,
saya rasa cara paling sederhana untuk mengimplementasikan AE di runtime bahasa baru memang memakai coroutine lalu memberi lapisan sintaks efek di atas struktur dasar
yield/resume.Saya bertanya apakah ada hal yang saya lewatkan.
Salah satu perbedaan utama AE dibanding coroutine adalah type safety.
Dalam AE, fungsi bisa menyatakan secara eksplisit efek apa yang dapat dipakainya di source code.
Misalnya jika bentuknya
query_db(): User can Database, maka ia bisa mengakses database dan saat dipanggil harus selalu diberikan handlerDatabase.Struktur ini sangat jelas menunjukkan batasan tentang apa yang bisa dan tidak bisa dilakukan.
Seperti server component di NextJS yang tidak bisa langsung memakai fitur client, batasan keamanan seperti ini populer di banyak bidang.
Effect-TS cukup mendekati pendekatan ini di JavaScript, yakni memanfaatkan coroutine, tetapi saya belum yakin apakah hasil akhirnya benar-benar ide yang bagus.
Mirip dengan DI di framework Spring, saya khawatir AE justru bisa menyebar ke seluruh kode dan hanya menambah kompleksitas.
Bahkan presentasi di EffectDays yang memperkenalkan cara memakai efek di frontend menurut saya kebanyakan hanya berisi boilerplate yang tidak bermakna.
AE memang konsep yang memikat, tetapi beban harus membungkus banyak hal ke dalam fungsi bisa mengganggu kemudahan khas JavaScript dalam menulis kode.
Di sisi lain, pendekatan seperti motioncanvas yang hanya memakai coroutine untuk mengekspresikan skenario grafis 2D yang kompleks dengan mudah juga punya keunggulan besar.
Video terkait EffectDays
MotionCanvas
Ada klaim bahwa di dalam thread, handler AE dapat
resumekode beberapa kali seperticall/cc.Sebaliknya, pada coroutine, setiap kali di-
yield, ia hanya bisa dilanjutkan sekali.Alur eksekusi yang tidak pasti seperti ini justru membuat prediksi menjadi lebih sulit, jadi saya lebih suka pendekatan yang secara eksplisit mengembalikan fungsi yang bisa dipanggil beberapa kali, atau menggantinya dengan struktur lain seperti iterator.
Sebagai abstraksi pemrograman, konsep ini terasa sangat menarik.
Saat dulu melakukan pemrograman kernel di Sun, saya merasa keuntungan besar ada pada kemampuan menulis kode secara ringkas setelah memanggil sesuatu seperti
sleep(foo)lalu dibangunkan kembali olehfoo.Beban harus menangani satu per satu berbagai edge case sebagai control flow jadi berkurang.
Selama isu locality memori diperhatikan, rasanya menyenangkan juga membayangkan banyak fungsi diinisialisasi lebih dulu dalam keadaan menunggu, lalu algoritme diekspresikan langsung sebagai mutasi dari tiap unit itu.
Terkait klaim "algebraic effects itu seperti exception yang bisa dilanjutkan",
saya bertanya apa bedanya secara praktis dengan type class
ApplicativeErroratauMonadError.Cara menyatakan efek yang bisa dipakai suatu fungsi mirip dengan checked exceptions, dan menangani efek dengan ekspresi
handlejuga hampir sama dengantry/catch.Type class seperti ini sudah mendukung cara menangkap exception melalui
handleError/handleErrorWithdan semacamnya.Dikatakan bahwa algebraic effects punya keunggulan untuk bahasa "masa depan", tetapi sebenarnya konsep ini juga sudah cukup bisa dipakai hari ini.
Tautan penjelasan cats
Jika hanya menangani satu efek, mungkin tidak ada banyak perbedaan, tetapi ketika beberapa efek dibutuhkan sekaligus, dukungan efek langsung jauh lebih rapi dan intuitif dibanding cara menumpuk monad secara eksplisit.
Saat menggabungkan monad, sering muncul masalah merepotkan seperti penentuan urutan atau harus mengubah urutan ketika hasil suatu fungsi tidak cocok dengan kumpulan monad yang diharapkan.
Secara pribadi saya pikir monad dan effect bukan hubungan saling bersaing, melainkan lebih tepat dipahami sebagai cara interpretasi yang saling melengkapi.
Lihat makalah terkait (misalnya makalah Koka).
Algebraic effects bekerja pada stack program seperti delimited continuation.
Dengan trik monad sederhana saja, tidak mungkin langsung melompat ke effect handler yang berada 5 stack frame di atas, lalu hanya mengubah variabel lokal di frame itu dan kembali lagi 5 frame ke bawah.
Perbedaannya ada pada perilaku statis vs dinamis.
Saat memprogram dengan monad, semua method terkait harus diimplementasikan sendiri, sedangkan dalam sistem effect, kita bisa memasang effect handler secara dinamis kapan saja dan dengan fleksibel menimpa handler yang sudah ada.
Misalnya, untuk testing, kita bisa memakai monad khusus dengan sifat IO di bagian bawah, lalu hanya di bawahnya lagi memasang effect handler, membentuk struktur yang lebih kompleks.
Kemiripannya memang besar, tetapi ada perbedaan dalam usability.
Algebraic effects mirip dengan monad
free, tetapi karena built-in, sintaksnya lebih mudah dan composability-nya juga lebih baik.Dalam bahasa yang berpusat pada monad seperti Haskell, efek yang sepintas mirip memang bisa dicapai berkat inferensi type class (gaya mtl) dan
bindbawaan (syntax).Saya awalnya salah paham bahwa algebraic effects hanya dibahas dalam static type system, tetapi belakangan saya tahu ada juga struktur dinamis di luar itu.
Dulu saya sangat terkesan oleh dua tulisan tentang versi dinamis Eff (pertama, kedua).
Konsep seperti "operasi terparameterisasi dengan generalized arity" juga terasa menarik saat menghubungkan abstraksi dengan pemrograman.
Disebutkan bahwa ini adalah konsep lama yang baru-baru ini muncul lagi dengan nama dan kerangka baru.
Pengenalan LISP Condition System
Pengalaman mencoba Algebraic Effects
Saya pernah mencoba protohackers dengan effects di OCaml 5 alpha.
Waktu itu cukup menyenangkan, meski toolchain-nya agak kurang nyaman.
Karena Ante memberi kesan serupa, saya menantikan perkembangannya.
Memang belum dilengkapi type system, tetapi sekarang jelas jauh lebih rapi.
Setelah menghabiskan banyak waktu di Prolog, saya sedang mencari bahasa yang memudahkan komposisi fungsi non-deterministik dan type checking saat compile time.
Ante adalah salah satu kandidat yang menarik perhatian saya.
Jangan lupakan juga developer tools dan plugin editor seperti LSP dan tree-sitter.
Menurut saya, tooling itu wajib ada sejak awal untuk bahasa baru.
Saya juga menganggap pengalaman debugging penting, jadi saya sedang mempertimbangkan apakah fitur replayability bisa disediakan secara default setidaknya dalam debug mode.
Terkait klaim "algebraic effects itu seperti exception yang bisa dilanjutkan",
saya bertanya apakah ini mirip dengan Common Lisp conditions.
Menarik melihat bagaimana konsep lama muncul kembali hanya dengan nama yang berbeda.
Algebraic effects jauh lebih luas daripada LISP condition system.
Dari sisi continuations yang bisa multi-shot, ini mirip dengan
call/ccdi Scheme.Disebutkan juga bahwa paralelisme semacam ini justru bisa menghasilkan akibat yang lebih buruk daripada jika tidak ada sama sekali.
Smalltalk memiliki "resumable exceptions".
Jika effect hanya dianggap sekadar penggantian nama untuk condition system lama, saya rasa diskusinya jadi sulit berkembang.
Algebraic effects yang dibahas saat ini memang memiliki perbedaan yang lebih dari sekadar konsep sederhana.
Dependency Injection juga bisa disebut dalam konteks yang mirip.