3 puan yazan GN⁺ 2025-01-20 | 2 yorum | WhatsApp'ta paylaş

Yan etkileri birinci sınıf değerler olarak ele almak

  • Haskell'de yan etkiler (ör. rastgele sayı üretme, çıktı verme vb.) “birinci sınıf değerler (first class value)” gibi ele alınır
  • Yani randomRIO(1, 6) gibi yan etki üreten bir fonksiyon çağrısı, sonuç değerin kendisini değil, “bir gün çalıştırılacak bir eylemi tanımlayan nesneyi” döndürür
  • Bu nesne gerçekten çalıştırıldığında rastgele bir değer üretir, ama ondan önce yalnızca bir yürütme planı içerir
  • IO Int gibi bir tür, “gerçekte çalıştırıldığında bir Int üreten eylemi” ifade eder; çağrı anında hemen çalışmaz, daha sonra ihtiyaç duyulan anda çalıştırılır
  • Bu özellik sayesinde, “fonksiyon çağrısı = anında yürütme” anlayışına sahip geleneksel prosedürel dillerden farklı olarak Haskell'de yan etkiler birleştirilebilir ve daha sonra gerçekten çalıştırılabilir

do bloklarının gizemini çözmek

  • do bloğu sihirli bir söz dizimi değildir; aslında yan etkileri bağlayan (bind) ve sırayla çalıştıran (then) iki operatörden oluşur

then

  • *> operatörü soldaki yan etkiyi çalıştırdıktan sonra sonuç değerini atar ve sağdaki yan etkiyi ardından çalıştırır
  • Örneğin putStr "hello" *> putStrLn "world", iki çıktıyı sırayla birleştiren tek bir IO () eylemi oluşturur
  • do bloğunda birden çok satır yazıldığında içeride bu tür sıralı yürütme operatörü kullanılır

bind

  • >>= operatörü, soldaki yan etkiyi çalıştırarak elde edilen değeri sağdaki fonksiyona aktarma görevini üstlenir
  • Örnek: randomRIO(1, 6) >>= print_side, zar sonucunu print_side fonksiyonuna iletip ekrana yazdıran bir yan etki oluşturur
  • do bloğundaki <- deseni, bu operatörü daha pratik biçimde ifade eden kavramdır

do bloklarının tamamı iki operatördür

  • Sonuç olarak do blokları *> ve >>= olmak üzere bu iki operatör üzerine kuruludur
  • Kod okunabilirliği ve pratiklik yüzünden do söz dizimi çok kullanılır, ama Haskell'in avantajlarını daha iyi kullanmak için bundan daha zengin yan etki birleştirme fonksiyonlarından yararlanmak gerekir

Yan etkiler üzerinde çalışan fonksiyonlar

  • Yan etkileri daha çeşitli biçimlerde ele almaya yarayan birçok fonksiyon standart kütüphanede bulunur

pure

  • pure x, “hiçbir ek yan etki olmadan x değerini sonuç olarak üreten bir eylem” oluşturur
  • Örnek: loaded_die = pure 4, her zaman 4 döndüren bir IO Int üretir

fmap

  • fmap :: (a -> b) -> IO a -> IO b biçimindedir; yan etkinin sonuç değerine saf bir fonksiyon uygulayarak yeni sonuç değeri üreten bir eylem oluşturur
  • Örneğin length <$> getEnv "HOME" ifadesi, ortam değişkenini alma yan etkisine length uygulayıp uzunluğunu hesaplayan bir eylem oluşturabilir

liftA2, liftA3, …

  • liftA2, liftA3 gibi fonksiyonlar, birden çok yan etki sonucunu tek bir saf fonksiyonla birleştirerek yeni bir yan etki üretir
  • Örnek: liftA2 (+) (randomRIO(1,6)) (randomRIO(1,6)), iki zar değerini toplayan bir yan etki oluşturur
  • Aynı iş, <$> ve <*> birleşimiyle de yapılabilir

Ara bölüm: mesele ne?

  • Bu yaklaşım diğer dillerde de mümkün olan basit bir özellik gibi görünebilir, ama Haskell'de yan etki eylemlerini istediğiniz zaman değişkenlere çıkarıp yeniden birleştirseniz bile yürütme zamanı ya da sonuç değişmez
  • Yan etkilerin bağımsız ele alınması sayesinde kodu refactor ederken daha az kafa karışıklığı olur ve eşitliksel akıl yürütmeye (equational reasoning) dayanan güvenli yeniden kullanım mümkün hale gelir

sequenceA

  • sequenceA [IO a] -> IO [a], “yan etki eylemleri listesi”ni “liste sonucu üreten tek bir yan etki eylemi”ne dönüştürür
  • Örneğin birden çok log eylemini listede toplayıp daha sonra sequenceA ile tek seferde çalıştırmak mümkündür
  • Sonsuz tekrar eden yan etkiler (ör. repeat (randomRIO(1,6))) bile listede tutulup sonra yalnızca gereken kadar take n ile alınıp sequenceA ile çalıştırılabilir

Ara not: kullanım kolaylığı sağlayan fonksiyonlar

  • void, sequenceA_, replicateM, replicateM_ gibi fonksiyonlar, sonuç değeri kullanılmadığında ya da tekrar eden çalıştırmalarda kullanışlıdır
  • Örneğin replicateM_ 500 (putStrLn "I will not cheat again."), tekrar sayısını doğrudan yönetmeden bir yan etkiyi çok kez çalıştırabilir

traverse

  • traverse :: (a -> IO b) -> [a] -> IO [b], listenin her öğesine yan etkili bir fonksiyon uygulayıp sonuçları listede toplayan bir eylem oluşturur
  • sequenceA aslında traverse id ile aynıdır; traverse_ ise sonuçları atan sürümdür

for

  • for, traverse ile aynı işleve sahiptir ama argümanları ters sırada alır

  • Örneğin for numbers $ \n -> ... biçimi, “for döngüsü” benzeri bir söz dizimini doğal şekilde ifade eder

  • Bu tür birleşimler sayesinde, başka dillerde ayrı söz dizimi gerektiren tekrar, dolaşım ve veri yapısı dönüşümleri Haskell'de kütüphane fonksiyonlarının birleşimiyle kurulabilir

Etkilerin birinci sınıf olma özelliğine yaslanmak

  • Haskell'de yan etkileri birinci sınıf değerler olarak etkin biçimde kullanmak, kod tekrarını azaltmaya ve yapıyı iyileştirmeye yardımcı olur
  • Örneğin önbellek kullanan büyük sayı asal çarpanlara ayırma mantığında IO yerine State kullanılarak, “yan etki var ama dış dünyayı etkilemiyor” türünde bir yapı kurulabilir
  • Bu şekilde yapılandırılmış yan etkiler yalnızca gerekli bölümlere uygulanır; geri kalan kod saf fonksiyon olarak kalabildiği için aynı anda hem güvenlik hem esneklik sağlanır
  • Son aşamada evalState gibi araçlarla gerçek yan etki yürütülüp sonuç saf bir değere dönüştürülebilir

Asla önemsemeniz gerekmeyen şeyler

  • Eski Haskell dönemlerinden kalan çeşitli adlar (>>, return, mapM vb.), güncel fonksiyonlarla (*>, pure, traverse vb.) değiştirilebilir
  • Bunlar “eski adlandırmalar ya da monad merkezli tasarım” kökenlidir; günümüzde ise Applicative ya da daha genel Functor temelli yaklaşım tavsiye edilir

Ek A: Başarıdan ve faydasızlıktan kaçınmak

  • “Haskell başarıdan kaçınır” sözü, “dilin popülerlik veya kullanım kolaylığı uğruna temel değerlerinden ödün vermemesi” anlamına gelir
  • “Haskell is useless” ifadesi ise başlangıçta yalnızca tamamen saf fonksiyonlara izin verdiği için gerçekten hiçbir şey yapamayan bir dil gibi görünmesini, daha sonra yan etkileri ‘birinci sınıf’ biçimde ele alan yaklaşımın eklenmesiyle pratiklik kazanmasını anlatan bir bağlamda kullanılır

Ek B: fmap neden hem yan etkiler hem listeler üzerinde eşleme yapar

  • fmap, çok genel bir biçime sahiptir: Functor f => (a -> b) -> f a -> f b; bu sayede liste, Maybe, IO gibi çeşitli kapsayıcı ya da yan etki türlerine ortak biçimde uygulanabilir
  • Listeye fmap uygularsanız fonksiyon tüm öğelere uygulanır; IO'ya uygularsanız sonuç değerine uygulanır
  • Bu şekilde “üzerine fonksiyon uygulanabilen yapı”ların tümüne Functor denir

Ek C: Foldable ve Traversable

  • Foldable, öğeler üzerinde dolaşıp işlem yapılabilen yapıdır
  • Traversable, yalnızca dolaşılabilen değil, aynı şekli yeni öğelerle yeniden kurabilen yapıdır
  • sequenceA ya da traverse'in özgün yapıyı koruyarak değer toplayabilmesi için ilgili yapının Traversable olması gerekir
  • Ağaç ya da Set gibi veri yapılarında yapı değerlere bağlı olarak değişebileceğinden, yalnızca dolaşım yapılabilen durumlar (Foldable) ile yapının gerçekten yeniden kurulabildiği durumlar (Traversable) ayrılır
  • Gerektiğinde listeye dönüştürüp ardından traverse kullanmak gibi yollarla yan etkiler esnek biçimde işlenebilir

2 yorum

 
bbulbum 2025-01-21

Reddit'te gezerken bu dil için sık sık reklam görüyorum.. Ama adı bile başlı başına hafif bir psikolojik eşik yaratıyor.
Nedense çok zor ve güçlü bir dilmiş gibi hissettiriyor..

 
GN⁺ 2025-01-20
Hacker News görüşleri
  • Haskell'in tip sistemi, diğer popüler dillerle karşılaştırıldığında karmaşıktır. Özellikle *>, <*>, <* gibi operatörler, kod tabanı genelinde öğrenme eğrisini artırır

    • Bir ay boyunca Haskell kullanmazsanız, üretkenliği korumak için >>= ve >> gibi operatörleri yeniden öğrenmeniz gerekir
    • Haskell kavramlarını insanlarla tartışmadan tek başına çalışırsanız zorlayıcı olabilir
  • Haskell, zorunlu programlamayı iyileştirmeye yardımcı olur

    • Birinci sınıf effect'ler ve pattern'ler kullanılarak boilerplate kod kaldırılabilir
    • Tip güvenliği sayesinde görece hatasız kod hızlıca yazılabilir
  • traverse/mapM'nin genelleştirilmiş sürümü, yalnızca listelerde değil tüm Traversable tiplerinde çalışır ve çok kullanışlıdır

    • traverse :: Applicative f => (a -> f b) -> t a -> f (t b) biçiminde kullanılabilir
    • Diğer dillerde benzer bir etki elde etmek için çok fazla kodu elle yazmak gerekirdi
  • Haskell güçlü monad'lara sahiptir ve bu da onu daha prosedürel hale getirir

    • do bloklarında ara değişkenler kullanılabilir
  • Haskell ile yazılmış yazılımlardan biri ImplicitCAD'dir

  • Haskell kodu prosedürel bir dil gibi okunur, ancak yan etkili fonksiyonlarla çalışırken avantajlar sağlar

    • IO monad'ı ile çalışmak karmaşıktır ve başka monad tipleri kullanmak istediğinizde daha da karmaşık hale gelir
  • >>, <i>> için eski addır ve iki operatör de sola birleşimlidir

    • >>, infixl 1 olarak tanımlanır ve <i>> ise infixl 4 olarak tanımlanır; bu yüzden <i>>, >>'den daha güçlü birleşir
  • Haskell'deki IO a ve a, asenkron ve senkrona benzer bir his verebilir

    • İlki, beklenmesi gereken bir promise/future döndürür
  • Diğer dillerde console.log("abc") gibi bir fonksiyonla basit IO yapılabilir

    • Bunun Haskell'deki IO'dan farkı olup olmadığı sorgulanır
  • Haskell'i hiç denememiş kişiler, GHC uzantıları kullanan gerçek Haskell'in fazla karmaşık olduğunu düşünebilir

    • Bu da Haskell'e olan ilgiyi azaltabilir