3 puan yazan GN⁺ 2025-05-25 | 1 yorum | WhatsApp'ta paylaş
  • 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 SayMessage gibi efekt fonksiyonu tanımları yapılabilir
  • foo () can SayMessage = ... biçiminde, bir fonksiyonun ilgili efekti kullanabileceği belirtilebilir
  • handle 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 map fonksiyonu, aldığı fonksiyonun keyfi bir e efektini 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ı: Yield efekti 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/set kullanacak ş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 Print gibi 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, replay gibi 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 IO eklenirse, 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

 
GN⁺ 2025-05-25
Hacker News görüşü
  • Bence iki dezavantajı var
    Verilen kod parçasına bakınca, foo ya da bar'ın başarısız olabileceğine dair hiçbir işaret olmaması ilk sorun
    Bu 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, foo ve bar'ı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ıp with ifadesini bulmanız, ardından da ilgili işleyiciyi takip ederek aşağı inmeniz gerekiyor
    Bu davranışı statik olarak izlemek ya da IDE'de doğrudan tanıma atlamak mümkün değil; çünkü my_function birden çok yerde farklı işleyicilerle çağrılabiliyor
    Bu 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-binding gibi çeşitli dinamik özelliklerle aynı şekilde, bağlama çağrı yığını boyunca yapılıyor
      Statik 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 ISomeEffectHandler benzeri 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 throw ile 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ün
      yield gibi iterator tarafına kadar derinlemesine inemediğini söylüyor

    • foo ve bar'ın başarısız olabileceğine dair görünür bir işaret olmaması asıl kilit nokta diye düşünüyorum
      Doğ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) -> B ifadesinde g ç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/resume temel yapısına efektleri sözdizimsel olarak giydirmek olduğunu düşünüyorum
    Acaba 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 Database biçimindeyse veritabanına erişebilir ve çağrılırken mutlaka bir Database işleyicisi sağlanmalıdır
      Ne 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 motioncanvas gibi 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/cc gibi, kodu birden fazla kez resume edebileceği söyleniyor
      Buna karşılık coroutine'lerde her yield yalnızca bir kez devam ettirilebilir
      Bu 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, foo tarafı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 ApplicativeError ya da MonadError type class'larından pratikte hangi yönüyle farklı olduğunu soruyor
    Bir fonksiyonda kullanılabilecek efektleri belirtme biçimi checked exception'lara benziyor; handle ifadesiyle efektleri işlemek de neredeyse try/catch ile aynı
    Bu type class'lar zaten handleError/handleErrorWith gibi mekanizmalarla exception yakalamayı destekliyor
    Cebirsel 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 free monad'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 (mtl tarzı) ve yerleşik bind sözdizimi sayesinde yüzeyde benzer bir etki üretilebiliyor

  • Baş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

    • Statik tip sistemlerinin hangi yönünü sevmediğini merak ediyor
  • 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

    • OCaml 5.3 sonrası effects eskisine göre çok daha iyi durumda
      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

    • Ante'nin yazarı olarak, şimdiden (çok temel olsa da) LSP desteği sunduğunu belirtiyor
      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 conditions ile benzer olup olmadığını soruyor
    Eski 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/cc yapısına benziyor
      Bö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