Haskell: Olağanüstü Bir Prosedürel Dil
(entropicthoughts.com)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 Intgibi 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
dobloğ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 birIO ()eylemi oluşturur dobloğ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 sonucunuprint_sidefonksiyonuna iletip ekrana yazdıran bir yan etki oluşturur dobloğundaki<-deseni, bu operatörü daha pratik biçimde ifade eden kavramdır
do bloklarının tamamı iki operatördür
- Sonuç olarak
doblokları*>ve>>=olmak üzere bu iki operatör üzerine kuruludur - Kod okunabilirliği ve pratiklik yüzünden
dosö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 birIO Intüretir
fmap
fmap :: (a -> b) -> IO a -> IO bbiç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 etkisinelengthuygulayıp uzunluğunu hesaplayan bir eylem oluşturabilir
liftA2, liftA3, …
liftA2,liftA3gibi 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
logeylemini listede toplayıp daha sonrasequenceAile 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 kadartake nile alınıpsequenceAile ç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şturursequenceAaslındatraverse idile aynıdır;traverse_ise sonuçları atan sürümdür
for
-
for,traverseile 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
IOyerineStatekullanı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
evalStategibi 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,mapMvb.), güncel fonksiyonlarla (*>,pure,traversevb.) 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
fmapuygularsanı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ırTraversable, yalnızca dolaşılabilen değil, aynı şekli yeni öğelerle yeniden kurabilen yapıdırsequenceAya datraverse'in özgün yapıyı koruyarak değer toplayabilmesi için ilgili yapınınTraversableolması 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
traversekullanmak gibi yollarla yan etkiler esnek biçimde işlenebilir
2 yorum
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..
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>>=ve>>gibi operatörleri yeniden öğrenmeniz gerekirHaskell, zorunlu programlamayı iyileştirmeye yardımcı olur
traverse/mapM'nin genelleştirilmiş sürümü, yalnızca listelerde değil tümTraversabletiplerinde çalışır ve çok kullanışlıdırtraverse :: Applicative f => (a -> f b) -> t a -> f (t b)biçiminde kullanılabilirHaskell güçlü monad'lara sahiptir ve bu da onu daha prosedürel hale getirir
dobloklarında ara değişkenler kullanılabilirHaskell 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
>>,<i>>için eski addır ve iki operatör de sola birleşimlidir>>,infixl 1olarak tanımlanır ve<i>>iseinfixl 4olarak tanımlanır; bu yüzden<i>>,>>'den daha güçlü birleşirHaskell'deki
IO avea, asenkron ve senkrona benzer bir his verebilirDiğer dillerde
console.log("abc")gibi bir fonksiyonla basit IO yapılabilirHaskell'i hiç denememiş kişiler, GHC uzantıları kullanan gerçek Haskell'in fazla karmaşık olduğunu düşünebilir