Cebirsel efektlere neden ihtiyaç var
(antelang.org)- Cebirsel efektler (effect handlers), çeşitli dil özelliklerinin (istisna işleme, generator'lar, coroutine'ler vb.) kütüphane düzeyinde uygulanabilmesini sağlayan esnek bir kontrol akışı aracıdır
- Fonksiyonel programlamada yaygın olan bağlam yönetimi, bağımlılık enjeksiyonu, global durumun yerine geçme vb. alanlarda da kullanılabilir
- API tasarımının sadeliğine ve kod içinde durum/ortam aktarımının otomatikleştirilmesine katkı sağlar
- Fonksiyonel saflığın korunması, yeniden oynatılabilirlik, güvenlik denetimi gibi konularda da avantaj sunar
- Son dönemdeki derleyici teknolojisi ilerlemeleri sayesinde performans sorunları da büyük ölçüde iyileşti
Cebirsel Efektlere (Algebraic Effects) Genel Bakış
Cebirsel efektler (ya da effect handlers), son dönemde öne çıkan bir programlama dili özelliğidir. Ante ve çeşitli araştırma dillerinin (Koka, Effekt, Eff, Flix vb.) temel özelliklerinden biri olarak hızla yaygınlaşmaktadır. Pek çok kaynak effect handler kavramını açıklar, ancak gerçekte buna neden ihtiyaç duyulduğuna dair derinlemesine açıklamalar görece azdır. Bu yazı, cebirsel efektlerin pratik kullanım alanlarını ve avantajlarını mümkün olduğunca geniş biçimde tanıtır.
Söz dizimi ve anlambilime hızlı bir bakış
- Cebirsel efektler, "yeniden başlatılabilir istisnalar" kavramına benzer
effect SayMessagegibi efekt fonksiyonu tanımları yapılabilirfoo () can SayMessage = ...biçiminde, bir fonksiyonun ilgili efekti kullanabileceği belirtilebilirhandle foo () | say_message () -> ...ile istisnalardaki try/catch'e benzer şekilde ele alınabilir
Bu temel yapı sayesinde efekt çağrıları ve kontrol akışı yönetilebilir.
Kullanıcı tanımlı kontrol akışını genişletme
Cebirsel efektlerin en büyük gerekçesi, tek bir dil özelliğiyle; normalde ayrı ayrı dil desteği gerektiren generator, istisna, coroutine, asenkronluk gibi özelliklerin kütüphane olarak uygulanabilmesidir.
- Fonksiyonlara çok biçimli efekt değişkenleri (
can e) verilerek, farklı efektler fonksiyon argümanı olarak aktarılabilir ve birleştirilebilir - Örneğin
mapfonksiyonu, aldığı fonksiyonun keyfi bireefektini kullanabilmesine izin verecek şekilde tanımlanabilir; böylece çıktı, asenkronluk gibi çeşitli etkilerle doğal biçimde birleşebilir
İstisna ve generator uygulama örnekleri
- İstisna uygulaması: Efekt tetiklendikten sonra
resumeçağrılmadan ele alınırsa, istisna ile aynı şekilde davranır - Generator uygulaması:
Yieldefekti tanımlanır; her değer yield edildiğinde dıştaki handler devreye girerek koşullara göre akışı denetleyebilir, filtreleme gibi gelişmiş kalıplar da nispeten basit kodla yazılabilir
Birden çok efektin bir arada kullanılabilmesi de, mevcut efekt soyutlama tekniklerine kıyasla önemli bir avantajdır.
Bir soyutlama katmanı olarak kullanım
Cebirsel efektler yalnızca çekirdek programlama özelliklerini genişletmek için değil, çeşitli iş uygulaması senaryolarında da oldukça kullanışlıdır.
Bağımlılık enjeksiyonu (Dependency Injection)
- Veritabanı, çıktı gibi bağımlı nesneler efekt olarak soyutlanıp handler'lar üzerinden yönetilebilir
- Test amaçlı mock nesnelerle değiştirme, çıktıyı yeniden yönlendirme gibi ihtiyaçlar da esnek biçimde karşılanabilir
Koşullu loglama veya çıktı yönetimi
- Log seviyesine göre log mesajlarının yazdırılıp yazdırılmayacağı merkezi olarak kontrol edilebilir
API tasarımını sadeleştirme ve Context aktarımını otomatikleştirme
Durum (State) efektinin kullanımı
- Context nesnesi ya da ortam bilgisinin aktarılması gereken durumlarda, efekt tabanlı olarak yalnızca
get/setkullanacak şekilde uygulandığında, açıkça parametre geçmeden durum yönetimi otomatikleştirilebilir - Normalde her fonksiyona context parametresi geçirilmesi gerekirken, state efekti bu ayrıntıyı gizleyebilir
Global nesnelerin yerine geçme
- Rastgele sayı üreteci, bellek ayırma gibi global nesne olarak yönetilen durumlar da efekt olarak soyutlanabilir; bu da kodun açıklığı, test kolaylığı ve eşzamanlılık desteği açısından avantaj sağlar
- Yalnızca handler değiştirilerek gerçek rastgele sayı kaynağı esnek biçimde değiştirilebilir
Doğrudan stil (Direct Style) yazımını destekleme
- Önceden option türleri, hata sarmalama gibi yöntemlerle birden çok nesnenin iç içe ele alınması gerekiyordu
- Efektler, bu tür sarmalamalar olmadan hata veya yan etki yollarını temiz biçimde ifade etmeyi sağlar
Saflığın korunması ve güvenlik denetimi
Yan etkilerin açıkça belirtilmesi
- Çoğu effect handler dilinde, yan etki oluşturan fonksiyonların tür imzasında
can IO,can Printgibi efektlerin zorunlu olarak belirtilmesi gerekir - Thread oluşturma, yazılım işlemsel belleği (STM) gibi alanlarda mutlaka saf fonksiyonlara ihtiyaç duyulur
Log yeniden oynatma ve deterministik ağ iletişimi
- Saflık temel alınarak
record,replaygibi handler'lar yazılabilir ve çalıştırma sonuçları yeniden üretilebilir - Hata ayıklama, veritabanları, oyun ağları vb. için deterministik sonuçlar ve rollback desteği sağlanabilir
Capability-based Security desteği
- Fonksiyon tür imzalarında işlenmemiş tüm efektler görünür olduğundan, harici kütüphanelerin güvenlik denetiminde etkilidir
- Daha önce yan etkisi olmayan bir fonksiyon güncellenip
can IOeklenirse, onu çağıran kod bunu hemen fark edebilir
Bununla birlikte, tüm efektler otomatik olarak yayıldığı için, farkında olmadan efektlerin ele alınmasına yol açan yan etkiler de ortaya çıkabilir.
Verimlilik açısından ve sonuç
- Eskiden çalışma verimliliği zayıf bir yön olarak görülse de, son dönemde tail-resumptive efektler gibi pek çok durumda optimizasyonlar ciddi ölçüde ilerledi
- Farklı dillerde closure call, evidence passing, handler specialization gibi etkili derleme stratejileri uygulanmaktadır
Cebirsel efektlerin geleceğin programlama dillerinde çok daha merkezi bir konuma yerleşmesi bekleniyor.
1 yorum
Hacker News görüşü
Bence iki dezavantajı var
Verilen kod parçasına bakınca,
fooya dabar'ın başarısız olabileceğine dair hiçbir işaret olmaması ilk sorunBu tür çağrıların bir hata işleyicisini tetikleyebileceğini anlamak için tip imzasını bizzat bulmak gerekiyor ve duruma göre IDE yardımı olmadan elle uğraşmak gerekebilir
İkincisi,
foovebar'ın başarısız olabileceğini fark ettikten sonra, gerçek bir başarısızlık durumunda hangi kodun çalıştığını bulmak için çağrı yığınında epey yukarı çıkıpwithifadesini bulmanız, ardından da ilgili işleyiciyi takip ederek aşağı inmeniz gerekiyorBu davranışı statik olarak izlemek ya da IDE'de doğrudan tanıma atlamak mümkün değil; çünkü
my_functionbirden çok yerde farklı işleyicilerle çağrılabiliyorBu kavramın çok taze olduğunu düşünüyorum ama sonuçta kod okunabilirliği ve hata ayıklama açısından kaygılarım var
Çalıştırma başarısız olduğunda hangi kodun devreye girdiğini bulma sorununa dair, bunun tam olarak dinamik kod enjeksiyonunun özü olduğu açıklanıyor
shallow-binding,deep-bindinggibi çeşitli dinamik özelliklerle aynı şekilde, bağlama çağrı yığını boyunca yapılıyorStatik analiz ya da IDE atlamasının mümkün olmaması da bu dinamik özellikten kaynaklanıyor
Ama bu süreçte aslında buna çok kafa yormaya gerek olmadığını düşünüyorum
Çünkü saf koda yalnızca efekt ekleyen bir yöntem; dolayısıyla bağlama, duruma göre ister saf ister saf olmayan efektler için, test mock'ları ya da prodüksiyon ortamı gibi farklı bağlamlarda yapılabiliyor
Bağımlılık enjeksiyonuna benzer bir ilke
Geleneksel monad'larla da benzer şeyler yapılabilir ama gerçekte monad'ın nerede örneklendiğini bulmak için yine çağrı yığınına bakmanız gerekir
Bu tekniklerin sağladığı faydalar var ama aynı zamanda bedelleri de açık
Test ve sandboxing için avantajlılar, ancak kodda ne olduğunun açıkça görünmesini sağlamıyorlar
Leksiksel efektler ve işleyiciler için IDE desteği üzerine lisans tezi yazmış biri olarak deneyimini paylaşıyor
Yukarıda işaret edilen tüm noktaların yeterince gerçekleştirilebilir olduğunu düşünüyor
Makale bağlantısı
.NET ekosisteminde arayüzlerin aşırı kullanılması eğiliminden söz ediliyor; bu da doğrudan metodun implementasyonuna atlamak için birkaç aşamadan geçme zahmeti yaratıyor
Bazen implementasyon başka bir assembly'deyse IDE özellikleri tamamen işe yaramaz hale gelebiliyor
Gelişmiş Dependency Injection'ta, özellikle Autofac'te, LISP'in dinamik kapsam değişkenleri gibi hiyerarşik scope'lar kurularak çalışma anında servislerin hangi instance'a bağlanacağı belirleniyor
Bu açıdan, bir efektin
ISomeEffectHandlerbenzeri bir arayüz instance'ı olarak enjekte edilip efekt gerçekleştiğinde ilgili metod çağrısıyla temsil edilmesi düşünülebilirİşleyicinin somut davranışı, örneğin exception fırlatması ya da loglama yapması, DI yapılandırmasına göre dinamik olarak belirlenir
Eskiden
throwile exception fırlatma kalıbı kullanılırdı ama efektleri arayüz temelli olarak açıkça belirtip işleme biçimini tamamen DI'a bırakacak bir tasarıma geçmek mümkünyieldgibi iterator tarafına kadar derinlemesine inemediğini söylüyorfoovebar'ın başarısız olabileceğine dair görünür bir işaret olmaması asıl kilit nokta diye düşünüyorumDoğrudan stil sayesinde efekt bağlamını dert etmeden kod yazabiliyorsunuz
Başarısızlık anında hangi kodun çalıştığını bulma meselesi de soyutlamanın doğasında var
Çalışma zamanında hangi efekt işleyicisinin gerçekten bağlanacağına daha sonra karar verilir
Tıpkı
f : g:(A -> B) -> t(A) -> Bifadesindegçalıştığında hangi kodun yürütüleceğini önceden bilememeniz gibiİşleyiciyi bulmak için çağrı yığını boyunca yukarı çıkmak gerektiğinden statik analizin imkânsız olduğu iddiasına katılmıyor
Pratikte statik analizin mümkün olduğunu ve IDE'de "caller'a git" benzeri özelliklerle hangi işleyicinin kullanıldığını seçebileceğinizi söylüyor
Ante'nin "sözde kodu" çok etkileyici
Haskell'in özellikleriyle Elixir'in ifade gücü ve pratikliğinin çok hoş bir birleşimi gibi
Geliştiriciler için Haskell izlenimi veriyor
Derleyicinin olgunlaşmasını bekliyorum
Ante ile uygulama geliştirmeyi denemek isterim
AE'nin (Algebraic Effects) kontrol akışını genelleştirerek coroutine'leri de uygulayabileceği iddiası hakkında
Aslında yeni bir dil çalışma zamanında AE'yi uygulamanın en basit yolunun, coroutine'leri kullanıp
yield/resumetemel yapısına efektleri sözdizimsel olarak giydirmek olduğunu düşünüyorumAcaba benim gözden kaçırdığım bir nokta mı var diye soruyor
AE'nin coroutine'lerden ayrıldığı başlıca nokta olarak tip güvenliği gösteriliyor
AE'de bir fonksiyonun kaynak kod düzeyinde hangi efektleri kullanabileceği belirtilebiliyor
Örneğin
query_db(): User can Databasebiçimindeyse veritabanına erişebilir ve çağrılırken mutlaka birDatabaseişleyicisi sağlanmalıdırNe yapabileceği ve ne yapamayacağına dair kısıtlar çok açık biçimde ortaya konuyor
NextJS gibi ortamlarda server component'ların client özelliklerini doğrudan kullanamaması gibi, bu tür güvenlik kısıtları birçok alanda popüler
Effect-TS, JavaScript'te bu yaklaşıma, yani coroutine kullanımına, yakın duruyor ama bunun gerçekten iyi bir fikir olup olmadığından emin değil
Spring framework'ündeki DI'a benzer şekilde, AE'nin kod tabanının tamamına yayılıp sadece ek karmaşıklık yaratmasından endişe ediyor
Nitekim EffectDays'te frontend efekt kullanımını anlatan sunumların çoğunun anlamsız boilerplate'ten ibaret olduğu eleştirisini yapıyor
AE çekici bir kavram olsa da, pek çok işi fonksiyonlarla sarmalama yükü JS'nin kendine has kolay kod yazma niteliğine zarar verebilir diye düşünüyor
Buna karşılık
motioncanvasgibi yalnızca coroutine'lerle karmaşık 2D grafik senaryolarını kolayca ifade edebilen yaklaşımların da büyük bir avantajı varİlgili video EffectDays
MotionCanvas
Bir thread içinde AE işleyicilerinin,
call/ccgibi, kodu birden fazla kezresumeedebileceği söyleniyorBuna karşılık coroutine'lerde her
yieldyalnızca bir kez devam ettirilebilirBu belirsiz yürütme akışı tahmini daha da zorlaştırdığı için, birden çok kez çağrılabilen fonksiyonları açıkça döndürmeyi ya da iterator gibi başka yapılar kullanmayı tercih ettiğini söylüyor
Kodlama soyutlaması olarak bu kavramın son derece çekici geldiğini söyleyen bir görüş
Sun'da kernel programlama yaparken
sleep(foo)gibi bir çağrıdan sonra,footarafından yeniden uyandırıldığında kodu kısa ve temiz yazabilmenin büyük avantaj olduğunu hissetmişÇeşitli edge case'leri kontrol akışıyla tek tek yönetme yükü azalıyor
Bellek yerelliğiyle ilgili meselelere dikkat edildiği sürece, çeşitli fonksiyonları önceden bekleme durumunda başlatıp algoritmayı her birimin mutasyonu olarak doğrudan ifade etmenin keyifli olacağını düşünüyor
"Cebirsel efektler, devam ettirilebilen exception'lar gibidir" iddiasına dair
Bunun
ApplicativeErrorya daMonadErrortype class'larından pratikte hangi yönüyle farklı olduğunu soruyorBir fonksiyonda kullanılabilecek efektleri belirtme biçimi checked exception'lara benziyor;
handleifadesiyle efektleri işlemek de neredeysetry/catchile aynıBu type class'lar zaten
handleError/handleErrorWithgibi mekanizmalarla exception yakalamayı destekliyorCebirsel efektlerin "geleceğin" dillerinde işe yarayacağı söyleniyor ama gerçekte bugün de yeterince kullanılan bir fikir
cats açıklama bağlantısı
Yalnızca tek bir efektle uğraşıyorsanız büyük bir fark olmayabilir ama aynı anda birden fazla efekte ihtiyaç duyulduğunda, doğrudan efekt desteği açıkça monad iç içe geçirme yaklaşımından çok daha temiz ve sezgisel oluyor
Monad'ları birleştirirken sıra belirleme ya da bazı fonksiyon sonuçlarının beklenen monad kümesiyle uyuşmaması halinde sırayı değiştirme gibi can sıkıcı sorunlar çıkabiliyor
Kişisel olarak monad'larla efektlerin rakip değil, birbirini tamamlayan yorumlama biçimleri olarak görülmesinin daha doğru olduğunu düşünüyor
İlgili makaleye bakılabilir: Koka makalesi
Cebirsel efektler, delimited continuation gibi, program yığını üzerinde çalışır
Sadece basit monad numaralarıyla, yığındaki 5 frame yukarıdaki efekt işleyicisine anında zıplayıp o frame'deki yerel değişkenleri değiştirip sonra tekrar 5 frame aşağı dönmek mümkün değildir
Fark, statik ve dinamik davranış arasındadır
Monad'larla programlarken ilgili tüm metodları doğrudan kendiniz uygulamanız gerekir; oysa efekt sisteminde herhangi bir anda dinamik olarak efekt işleyicisi kurup mevcut işleyiciyi esnek şekilde override edebilirsiniz
Örneğin test için altta özel bir IO özellikli monad kullanıp, onun da altında efekt işleyicileri kurulan bileşik bir yapı oluşturmak mümkündür
Benzerlikler büyük ama kullanılabilirlikte fark var
Cebirsel efektler
freemonad'a benzer bir yapıya sahip fakat dilin içine gömülü olduğundan sözdizimi daha kolay ve composability daha güçlüHaskell gibi monad merkezli dillerde type class inference (
mtltarzı) ve yerleşik bind sözdizimi sayesinde yüzeyde benzer bir etki üretilebiliyorBaşta cebirsel efektlerin yalnızca statik tip sistemlerinde ele alındığını sanmış ama yakın zamanda bunun dışında dinamik yapılar da olduğunu öğrenmiş
Eskiden Eff'in dinamik sürümüyle ilgili yazılmış iki yazıyı özellikle etkileyici bulmuş: birincisi, ikincisi
"Genelleştirilmiş arity'ye sahip parametreli işlemler" gibi kavramların da soyutlamayı programlamaya bağlama açısından ilginç olduğunu düşünüyor
Eski bir kavramın son zamanlarda yeni bir isim ve çerçeveyle yeniden ortaya çıktığını belirtiyor
LISP Condition System tanıtımı
Algebraic Effects deneyimi
OCaml 5 alpha'da effects kullanarak protohackers yaptığı deneyiminden bahsediyor
Eğlenceliydi ama o dönemde toolchain biraz rahatsız ediciydi
Ante de benzer bir his verdiği için ileride nasıl gelişeceğini merak ediyor
Henüz bir tip sistemi eklenmiş değil ama şimdi kesinlikle daha derli toplu
Prolog'da çok zaman geçirmiş biri olarak, belirsiz-deterministik olmayan fonksiyon bileşimini ve derleme zamanında tip denetimini kolaylaştıracak bir dil aradığını söylüyor
Ante de bu adaylardan biri olarak ilgisini çekiyor
LSP, tree-sitter gibi geliştirici araçlarını ve editör eklentilerini de unutmamak gerektiğini ekliyor
Yeni bir dilde en baştan araç desteğinin şart olduğuna inanıyor
Hata ayıklama deneyimini de önemsediği için, en azından debug mode'da replayability özelliğini varsayılan sunmanın yollarını araştırıyor
"Cebirsel efektler, devam ettirilebilen exception'lar gibidir" iddiasına dair
Bunun Common Lisp
conditionsile benzer olup olmadığını soruyorEski bir kavramın sadece yeniden adlandırılarak geri dönmesi açısından ilginç buluyor
Cebirsel efektler, LISP condition system'den çok daha kapsamlı
Continuation'ların multi-shot olabilmesi bakımından Scheme'in
call/ccyapısına benziyorBöyle bir paralelliğin hiç olmamasından bile daha kötü sonuçlar doğurabileceği de belirtiliyor
Smalltalk'ta "resumable exceptions" var
Efektleri yalnızca eski condition system'in yeniden adlandırılmış hali gibi görmek, tartışmayı ilerletmeyi zorlaştırır diye düşünüyor
Şu an tartışılan cebirsel efektlerin, salt bir kavramdan öte, somut farkları var
Dependency Injection da benzer bir bağlamda anılabilir