README.md - Grip

This is the Turkish translation of Yann Esposito's article Learn Haskell Fast and Hard.

Yann Esposito'nun Learn Haskell Fast and Hard başlıklı yazısının Türkçe çevirisidir.

Zor Yoldan Haskell

TL, DR (Çok uzundu okumadım): Haskell öğrenmek için kısa ve yoğun bir rehber.

İçindekiler:


Tüm geliştiricilerin Haskell öğrenmesi gerektiğine inanıyorum. Herkesin süper Haskell ninjası olması gerektiğini düşünmüyorum, ama herkes Haskell'in sahip olduğu farklı yönleri görmeli; Haskell öğrenmek zihninizi acar.

Anaakım diller aynı temelleri paylaşırlar:

  • değişkenler
  • döngüler
  • işaretçiler (pointer) [^fn-1]
  • veri yapıları, nesneler ve sınıflar (genellikle)

Ama Haskell çok farklıdır. Bu dil daha önceden hiç duymamış olduğum bir sürü kavram kullanıyor. Bu kavramların çoğu daha iyi bir programcı olmanızda yardımcı olacaktır.

Haskell öğrenmek zor olabilir, benim için öyleydi. Bu yazıda ben öğrenirken eksik olan şeyleri size sunmaya çalışacağım.

Bu yazıyı takip etmek zor olacak ve bunu bilerek yapıyorum; Haskell öğrenmenin kısayolu yoktur, zordur ve çaba ister. Ama bunun iyi bir şey olduğuna inanıyorum; Haskell, zor olduğu için ilginç.

Haskell öğrenmenin klasik yolu şu iki kitabı okumaktır. İlk önce "Learn You a Haskell" (Haskell Öğrenin) ve sonrasında da "Real World Haskell" (Gerçek Dünyada Haskell). Ben de bunun doğru yol olduğuna inanıyorum. Haskell'in doğru düzgün öğrenmek için, bu kitapları ayrıntılı şekilde okumalısınız.

Tersi şekilde, bu yazı Haskell'in ana konularının oldukça kısa ve yoğun bir özeti. Kendim Haskell öğrenirken ihtiyaç duyup bulamadığım bazı bilgileri de ekledim.

Bu yazının beş bölümü var:

  • Giriş: Haskell'in insancıl olabildiğini göstermek için bir kısa örnek.
  • Temel Haskell: Haskell söz dizimi ve bazı temel kavramlar.
  • Zor Bölüm:
    • Fonksiyonel stil; imperatif stilden fonksiyonel stile kademeli bir örnek.
    • Tipler; tipler ve standard bir ikili ağaç (binary tree) örneği.
    • Sonsuz Yapılar; sonsuz bir ikili ağacı işleyin.
  • Çok Zor Bölüm:
    • IO ile baş edin; minimal bir örnek.
    • IO hileleri açıklaması, IO'yu anlamak için ihtiyaç duyduğum gizli detay
    • Monad'ler, inanılmaz genellenebilirlikleri
  • Ek:
    • Sonsuz ağaçlar hakkında matematik tabanlı bir tartışma.

Her .lhs ile biten bir dosya isimli ayırıcı gördüğünüzde, dosyaya ulaşmak için tıklayabilirsiniz. Dosyayı dosyaismi.lhs diye kaydederseniz, runhaskell dosyaismi.lhs komutuyla çalıştırabilirsiniz. Bazıları çalışmayabilir ama çoğu çalışacaktır. Aşağıda bir link görebilirsiniz. 01_basic/10_Introduction/00_hello_world.lhs (Çevirmen notu: Kodlardaki karakter dizileri Türkçeleştirilmiş, ancak değişken isimleri aynı bırakılmıştır. İndirilen kodlar İngilizce kaynaktan olup Türkçeleştirilmemiş olacaktır.)

1. Giriş

1.1. Kurulum

Haskell

Araçlar:

  • ghc: C'deki gcc gibi bir derleyici.
  • ghci: İnteraktif Haskell yorumlayıcısı. (REPL)
  • runhaskell: Bir programı derlemeden çalıştırmak için kullanılır. Kolaydır, ama derlenen programlara göre çok yavaştır.

1.2. Korkmayın

Scream

Haskell hakkındaki pek çok kitap/makale az bilinen bir formülü (quick sort, Fibonacci, vs.) yazmakla başlıyor, bense tam tersini yapacağım. İlk önce size Haskell'in süper güçlerini göstermeyeceğim. Haskell ve diğer programlama dilleri arasındaki benzerliklerle başlayacağım. Zorunlu "Merhaba Dünya" programıyla başlayalım.

main = putStrLn "Merhaba Dünya!"

Çalıştırmak için, kodu merhaba.hs olarak kaydedip şu komutları kullanabilirsiniz:

~ runhaskell ./merhaba.hs
Merhaba Dünya!

Doğrudan kaynak kodunu da indirebilirsiniz. Aşağıdaki komutların hemen altında linki görüp, 00_hello_world.lhs olarak kaydedip bu komutlarla çalıştırabilirsiniz:

~ runhaskell 00_hello_world.lhs
Hello World!

01_basic/10_Introduction/00_hello_world.lhs


01_basic/10_Introduction/10_hello_you.lhs Şimdi, adınızı soran ve aldığı cevapla size "Merhaba" diyen bir program yazalım:

main = do
    print "Adiniz nedir?"
    name <- getLine
    print ("Merhaba " ++ name ++ "!")

Öncelikle, bunu birkaç imperatif dildeki benzer programlarla karşılaştıralım:

# Python
print "Adiniz nedir?"
name = raw_input()
print "Merhaba %s!" % name
# Ruby
puts "Adiniz nedir?"
name = gets.chomp
puts "Merhaba #{name}!"
// In C
#include <stdio.h>
int main (int argc, char **argv) {
    char name[666]; // <- musibetli sayi!
    // Adim 665 karakterden fazlaysa ne olacak?
    printf("Adiniz nedir?\n");
    scanf("%s", name);
    printf("Merhaba %s!\n", name);
    return 0;
}

Yapı aynı, ama söz dizimsel farklılıklar var. Bu yazının ana kısmı bu farkların sebebini açıklamak üzerine olacak.

Haskell'de bir main fonksiyonu vardır ve her nesnenin bir tipi vardır. main'in tipi IO ()'dur. Bu, main yan etkilerde bulunacak demektir.

Şimdilik, Haskell'in anaakım imperatif dillere benzer görünebileceğini hatırlamanız yeterli.

01_basic/10_Introduction/10_hello_you.lhs


01_basic/10_Introduction/20_very_basic.lhs

1.3. Haskell'e Giriş

Very Basic

Devam etmeden önce, Haskell'in bazı temel özelliklerinin farkına varmanız gerekiyor.

Fonksiyonel

Haskell fonksiyonel bir dildir. Eğer imperatif bir dilde geçmişiniz varsa, yeni bir sürü şey öğrenmeniz gerekiyor. Umarım bu yeni kavramlar size imperatif dillerde program yazarken bile yardımcı olur.

Akıllı Statik Tip Sistemi

Tip sistemi, C'de, C++'ta, Java'da olduğu gibi sizi engellemek yerine, size yardım etmek için var.

Saflık

Genellikle fonksiyonlarınız dış dünyada bir şeyi değiştirmeyecekler. Bu demek oluyor ki, bir değişkenin değerini değiştiremeyecekler, kullanıcıdan girdi alamayacaklar, ekrana yazı yazamayacaklar, veya bir füzeyi ateşleyemeyecekler. Diğer yandan, paralellik sağlamak çok kolay olacak. Haskell nerede yan etkilerin olduğunun ve nerede kodunuzun saf olduğunun ayrımını çok açık bir şekilde yapar. Ayrıca, programınız hakkında mantık yürütmek de çok daha kolay olur. Çoğu hata, kodunuzun saf kısmında engellecektir.

Daha da ötesi, Haskell'de saf fonksiyonlar temel bir kural izlerler:

Bir fonksiyona aynı parametreleri vermek her zaman aynı değerleri döndürür.

Tembellik

Tembellik, genelde alışılmadık bir dil tasarım tercihidir. Haskell'de varsayılan olarak her şey sadece ihtiyaç olduğunda hesaplanır / işlenir. Bunun sonuçlarından biri de sonsuz yapıları işlemek için çok mükemmel bir yol sunmasıdır.

Son uyarı da Haskell kodunu nasıl okumanız gerektiğiyle ilgili. Benim için, bilimsel makaleleri okumak gibi. Bazı kısımları çok açık, ama bir formül gördünüzde odaklanın ve yavaşça okuyun. Ayrıca, Haskell öğrenirken, garip söz dizimsel detayları anlamamanız gerçekten önemli değil. Ama eğer >>=, <$>, <- v.b. herhangi bir garip sembol görürseniz, görmezden gelin ve kodun akışını takip edin.

1.3.1. Fonksiyon tanımı

Şu şekilde fonksiyon tanımlamaya alışmış olabilirsiniz:

C'de:

int f(int x, int y) {
    return x*x + y*y;
}

JavaScript'te:

function f(x,y) {
    return x*x + y*y;
}

Python'da:

def f(x,y):
    return x*x + y*y

Ruby'de:

def f(x,y)
    x*x + y*y
end

Scheme'de:

(define (f x y)
    (+ (* x x) (* y y)))

Son olarak, Haskell yolu da budur:

f x y = x*x + y*y

Tertemiz. Parantez yok, def yok.

Unutmayın, Haskell fonksiyonları ve tipleri sıkça kullanır. Bu yüzden, onları tanımlamak oldukça kolaydır. Söz dizimi, özellikle öyle düşünülmüştür.

1.3.2. Tip örneği

Zorunlu olmamasına rağmen, fonksiyonlar için tip bilgisi genellikle ayrıca girilir. Zorunlu değildir, çünkü derleyici sizin için çıkarım yapacak kadar akıllıdır. Yine de tipleri yazmak iyi bir fikirdir, çünkü kodun anlaşılmasına yardımcı olur.

Bakalım nasıl oluyormuş:

-- Tipleri belirtmek icin :: isaretini kullaniyoruz
f :: Int -> Int -> Int
f x y = x*x + y*y

main = print (f 2 3)
~ runhaskell 20_very_basic.lhs
13

01_basic/10_Introduction/20_very_basic.lhs


01_basic/10_Introduction/21_very_basic.lhs

Şimdi bunu deneyelim:

f :: Int -> Int -> Int
f x y = x*x + y*y

main = print (f 2.3 4.2)

Şu hatayı almış olmalısınız:

21_very_basic.lhs:6:23:
    No instance for (Fractional Int)
      arising from the literal `4.2'
    Possible fix: add an instance declaration for (Fractional Int)
    In the second argument of `f', namely `4.2'
    In the first argument of `print', namely `(f 2.3 4.2)'
    In the expression: print (f 2.3 4.2)

Sorun şu: 4.2 bir tam sayı (Int) değil.

01_basic/10_Introduction/21_very_basic.lhs


01_basic/10_Introduction/22_very_basic.lhs

Çözümü de şu: f fonksiyonu için şimdilik bir tip belirtmeyelim ve Haskell'in tip çıkarımı yapmasına izin verelim.

f x y = x*x + y*y

main = print (f 2.3 4.2)

Çalışıyor! Ne şanslıyız ki, her tip için yeni bir fonksiyon tanımlamak zorunda değiliz. Örneğin C'de, int için, float için, long için, double için vs. ayrı ayrı fonksiyon tanımlamak zorundayız.

Peki hangi tipi belirtmeliydik? Haskell'in bizim için bulduğu tipi görmek için ghci'yi başlatın:

% ghci
GHCi, version 7.0.4: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
Prelude> let f x y = x*x + y*y
Prelude> :type f
f :: Num a => a -> a -> a

Hi? Bu garip tip de neyin nesi?

Num a => a -> a -> a

İlk önce, sağdaki a -> a -> a kısmına bakalım. Anlamak için kademeli olarak şu örnekleri inceleyelim:

Yazılı Tip Anlamı
Int Int tipi
Int -> Int Int'ten Int'e olan fonksiyon tipi
Float -> Int Float'tan Int'e olan fonksiyon tipi
a -> Int herhangi bir tipten Int'e olan fonksiyon tipi
a -> a herhangi bir a tipinden aynı a tipine olan fonksiyon tipi
a -> a -> a herhangi bir a tipinden iki argümanın aynı a tipine olan fonksiyon tipi

a -> a -> a tipinde, a harfine tip değişkeni diyoruz. (type variable). Bu f'nin iki argümanı olduğu, ve girilen argümanlar ve fonksiyon sonucunun aynı tipten olduğu anlamına geliyor. Tip değişkeni a, başka bir sürü değer alabilir. Örneğin Int, Integer, Float...

Yani C'deki gibi zorunlu olarak bir fonksiyon için int, long, float, double vs. gibi tip belirtmek yerine, herhangi bir dinamik tip sistemli dil gibi sadece bir fonksiyon tanımlıyoruz.

Buna bazen parametrik çokşekillilik (parametric polymorphism) de deniyor. Hayatta her istediğinizin olması gibi bir şey.

Genellikle a herhangi bir tip olabilir, örneğin String veya Int, ama Trees gibi daha karışık tipler, başka fonksiyonlar da olabilir. Ama buradaki tipimiz Num a => ile başlıyor.

Num bir tip sınıfı. (type class). Tip sınıfları tip grupları gibi düşünülebilir. Num sınıfı sadece sayı gibi davranan tipleri içerir. Daha doğrusu, Num, belli bir fonksiyon listesinin, özellikle (+) ve (*) fonksiyonlarının, etki ettiği sınıftır.

Tip sınıfları güçlü bir dil yapısıdır. Onlarla inanılmaz güçlü şeyler yapabiliriz. Buna daha sonra tekrar değineceğiz.

Sonuç olarak, Num a => a -> a -> a şu demek oluyor:

Diyelim ki a, Num tip sınıfına ait bir tip. Bu da a tipinden a -> a tipine bir fonksiyon.

Evet, garip. Aslında Haskell'de hiçbir fonksiyonun gerçekten iki argümanı yoktur. Onun yerine, her fonksiyonun sadece bir argümanı vardır. Ama hatırlamalıyız ki iki argüman almakla, bir argüman alıp ikinci argümanı bir parametre olarak alan bir fonksiyon döndürmek denk şeylerdir.

Daha açık olmak gerekirse, f 3 4, (f 3) 4'e denktir. f 3'un de bir fonksiyon olduğuna dikkat edin:

f :: Num a => a -> a -> a

g :: Num a => a -> a
g = f 3

g y  3*3 + y*y

Fonksiyonlar için bir notasyon daha var. Lambda notasyonu isimsiz fonksiyonlar yaratmamıza olanak sağlar. Bunlara anonim fonksiyonlar diyoruz. Aynı şeyi lambda notasyonuyla şöyle de yazabilirdik:

g = \y -> 3*3 + y*y

Burada \ kullanılıyor, çünkü λ (lambda) harfine benziyor, ve aynı zamanda ASCII dizisine dahil.

Eğer fonksiyonel programlamaya alışık değilseniz beyniniz yanmaya başlamış olmalı. Artık gerçek bir uygulama yazma zamanı.

01_basic/10_Introduction/22_very_basic.lhs


01_basic/10_Introduction/23_very_basic.lhs

Ama ondan önce, tip sisteminin beklediğimiz gibi çalıştığını doğrulayalım:

f :: Num a => a -> a -> a
f x y = x*x + y*y

main = print (f 3 2.4)

Çalışıyor, çünkü 3 hem Float gibi kesirli (Fractional) sayılar için, hem de Integer (tam sayı tipi) için geçerli bir gösterim. 2.4 kesirli bir sayı olduğu için 3 de kesirli bir sayı olarak yorumlanıyor.

01_basic/10_Introduction/23_very_basic.lhs


01_basic/10_Introduction/24_very_basic.lhs

Eğer fonksiyonumuzu farklı tiplerle çalışmaya zorlarsak, hata verecektir:

f :: Num a => a -> a -> a
f x y = x*x + y*y

x :: Int
x = 3
y :: Float
y = 2.4
main = print (f x y) -- calismayacak, cunku tip x ≠ tip y

Derleyici hata veriyor. İki parametre de aynı tipten olmak zorunda.

Eğer bunun kötü bir fikir olduğunu ve derleyicinin sizin için bir tipten diğerine dönüşümü yapması gerektiğini düşünüyorsanız, bu müthiş (ve komik) videoyu mutlaka izlemelisiniz: WAT

01_basic/10_Introduction/24_very_basic.lhs

2. Temel Haskell

Essential

Bu kısma yalnızca göz gezdirmenizi tavsiye ediyorum. Her zaman yararlanacağınız bir kaynak gibi düşünün. Haskell'in birçok özelliği vardır. Burada da pek çoğu eksik. Eğer notasyon garip gelirse tekrar buraya dönün.

İki ifadenin denk olduğunu belirtmek için işaretini kullanıyorum. Bu sahte bir notasyon, Haskell'de mevcut değil. Aynı şekilde, bir ifadenin hesaplanan değerinin ne olduğunu belirtmek için de işaretini kullanacağım.

2.1. Notasyon

Aritmetik

3 + 2 * 6 / 3  3 + ((2*6)/3)

Mantıksal

True || False  True
True && False  False
True == False  False
True /= False  True  (/=) esit degildir operatorudur

Üslü Sayılar

x^n     herhangi bir n integral tipi icin (Int veya Integer olarak anlayin)
x**y    herhangi bir y sayi tipi icin (ornegin Float)

Integer'ın bilgisayarınızın kapasitesi dışında bir sınırı yoktur.

4^103
102844034832575377634685573909834406561420991602098741459288064

Evet! Ayrıca rasyonel sayılar da var! Ama önce Data.Ratio modülünü içeri aktarmanız gerekiyor:

$ ghci
....
Prelude> :m Data.Ratio
Data.Ratio> (11 % 15) * (5 % 3)
11 % 9

Listeler

[]                      ⇔ bos liste
[1,2,3]                 ⇔ integral listesi
["foo","bar","baz"]     ⇔ String listesi
1:[2,3]                 ⇔ [1,2,3], (:) bir elemani one ekleme
1:2:[]                  ⇔ [1,2]
[1,2] ++ [3,4]          ⇔ [1,2,3,4], (++) birlestirme
[1,2,3] ++ ["foo"]      ⇔ HATA String ≠ Integral
[1..4]                  ⇔ [1,2,3,4]
[1,3..10]               ⇔ [1,3,5,7,9]
[2,3,5,7,11..100]       ⇔ HATA! O kadar da akilli degilim!
[10,9..1]               ⇔ [10,9,8,7,6,5,4,3,2,1]

Karakter Dizileri

Haskell'de String tipi, Char tipinden oluşmuş listeye denktir.

'a' :: Char
"a" :: [Char]
""    ⇔ []
"ab"  ⇔ ['a','b'] ⇔  'a':"b" ⇔ 'a':['b'] ⇔ 'a':'b':[]
"abc" ⇔ "ab"++"c"

Dikkat: Gerçek kodda, yazıyı temsil etmek için Char listesi kullamamalısınız. Genel olarak Data.Text kullanmalısınız. Eğer ASCİİ karakter akımını (stream) temsil etmek istiyorsanız, onun için de Data.ByteString kullanmalısınız.

Demetler (Tuple)

Bir ikili demetin tipi (a,b)'dir. Demetlerdeki elemanlar farklı tipte olabilirler.

-- Tum bu demetler gecerlidir
(2,"foo")
(3,'a',[2,3])
((2,"a"),"c",3)

fst (x,y)         x
snd (x,y)         y

fst (x,y,z)       HATA: fst :: (a,b) -> a
snd (x,y,z)       HATA: snd :: (a,b) -> b

Parantezlerle Başa Çıkmak

Bazı parantezlerden kurtulmak için ($) ve (.) fonksiyonlarını kullanabilirsiniz.

-- Aslinda:
f g h x           (((f g) h) x)

-- $ isareti kendisinden ifadenin sonuna
-- kadar olan parantezin yerine gecer
f g $ h x         f g (h x)  (f g) (h x)
f $ g h x         f (g h x)  f ((g h) x)
f $ g $ h x       f (g (h x))

-- (.) kompozisyon fonksiyonu
(f . g) x         f (g x)
(f . g . h) x     f (g (h x))

01_basic/20_Essential_Haskell/10a_Functions.lhs

2.2. Fonksiyonlar için Faydalı Notasyonlar

Hatırlatma:

x :: Int             x Int tipinde herhangi bir deger alabilir
x :: a               x herhangi bir tip olabilir
x :: Num a => a      x Num tip sinifina dahil olan
                         herhangi bir a tipi olabilir
f :: a -> b          f a'dan b'ye bir fonksiyondur
f :: a -> b -> c     f a'dan (bc)'ye bir fonksiyondur
f :: (a -> b) -> c   f (ab)'den c'ye bir fonksiyondur

Hatırlayın ki bir fonksiyonu tanımlamadan önce tipini belirtmek zorunlu değil. Haskell genelde tip çıkarımını sizin için kendisi yapar. Ama genelde tipleri belirtmek iyi uygulama olarak görülür.

Orta notasyon

square :: Num a => a -> a
square x = x^2

^ işaretinin orta notasyon kullandığına dikkat edin. Her orta notasyon için bir başta notasyon vardır. Sadece parantez içine koymak durumundasınız.

square' x = (^) x 2

square'' x = (^2) x

Soldaki ve sağdaki x'leri silebiliriz. Buna η (eta) sadeleştirmesi deniyor.

square''' = (^2)

Değişken isimlerinde ' kullanabildiğimize dikkat edin.

squaresquare'square''square'''

Testler

Mutlak değer fonksiyonu yazalım:

absolute :: (Ord a, Num a) => a -> a
absolute x = ıf x >= 0 then x else -x

Dikkat edin ki Haskell'deki if .. then .. else notasyonu, C'deki ¤?¤:¤ operatörüne çokça benziyor. else kısmını unutmanız mümkün değil.

Denk başka bir versiyonu:

absolute' x
    | x >= 0 = x
    | otherwise = -x

Haskell'de paragraf başı / boşluklar önemlidir. Python'daki gibi, kötü boşluklar kodunuzu bozabilir.

01_basic/20_Essential_Haskell/10a_Functions.lhs

3. Zor Kısım

Zor kısım şimdi başlıyor.

3.1. Fonksiyonel Stil

Functional

Bu bölümde, Haskell'in etkileyici yeniden yapılandırma (refactoring) yeteneklerini göreceğiz. Bir problem seçip önce standart imperatif yolla çözeceğiz. Daha sonra kodun evrimini göreceğiz, son hali çok daha zarif ve kolay anlaşılabilir olacak.

Aşağıdaki problemi çözelim:

Verilen bir tam sayı listesindeki çift sayıların toplamını alın. Örnek: [1,2,3,4,5] ⇒ 2 + 4 ⇒ 6

Fonksiyonel ve imperatif yaklaşımların arasındaki farkı göstermek için, imperatif çözümü göstererek başlayacağım: (JavaScript'te)

function evenSum(list) {
    var result = 0;
    for (var i=0; i< list.length ; i++) {
        if (list[i] % 2 ==0) {
            result += list[i];
        }
    }
    return result;
}

Haskell'de, farklı olarak, değişkenler veya for döngüleri yoktur. Döngüler olmaksızın aynı sonucu elde etmenin bir yolu özyinelemedir. (recursion)

Dikkat: Özyineleme imperatif dillerde genellikle yavaş olarak algılanır. Fonksiyonel programlamada genellikle durum bu değildir. Çoğu zaman Haskell özyinelemeli fonksiyonları verimli şekilde işler.

İşte özyinelemeli fonksiyonun C versiyonu. Basitlik için tam sayı listesinin ilk 0 değeri ile bittiğini varsaydığıma dikkat edin.

int evenSum(int *list) {
    return accumSum(0,list);
}

int accumSum(int n, int *list) {
    int x;
    int *xs;
    if (*list == 0) { // eger liste bossa
        return n;
    } else {
        x = list[0]; // x listenin ilk elemani olsun
        xs = list+1; // xs listenin ilk elemani haric geri kalani olsun
        if ( 0 == (x%2) ) { // eger x ciftse
            return accumSum(n+x, xs);
        } else {
            return accumSum(n, xs);
        }
    }
}

Bu kodu aklınızda tutun. Şimdi onu Haskell'e çevireceğiz. Ama ilk önce, size burada kullanacağımız üç basit ama kullanışlı fonksiyonu tanıtmam gerekiyor:

even :: Integral a => a -> Bool
head :: [a] -> a
tail :: [a] -> [a]

even bir sayının çift olduğunu doğrular.

even :: Integral a => a -> Bool
even 3   False
even 2   True

head bir listenin ilk elemanını döndürür.

head :: [a] -> a
head [1,2,3]  1
head []       HATA

tail bir listenin ilk elemanı hariç tüm elemanlarını döndürür.

tail :: [a] -> [a]
tail [1,2,3]  [2,3]
tail [3]      []
tail []       HATA

Görebileceğiniz üzere, herhangi bir boş olmayan l listesi için, l ⇔ (head l):(tail l)


02_Hard_Part/11_Functions.lhs

İlk Haskell çözümümüz. evenSum fonksiyonu bir listedeki tüm çift sayıların toplamını döndürür.

-- Versiyon 1
evenSum :: [Integer] -> Integer

evenSum l = accumSum 0 l

accumSum n l = if l == []
                  then n
                  else let x = head l
                           xs = tail l
                       in if even x
                              then accumSum (n+x) xs
                              else accumSum n xs

Fonksiyonu denemek için ghci'ı kullanabilirsiniz.

% ghci
GHCi, version 7.0.3: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> :load 11_Functions.lhs
[1 of 1] Compiling Main             ( 11_Functions.lhs, interpreted )
Ok, modules loaded: Main.
*Main> evenSum [1..5]
6

Burada çalıştırılma örneğini görebilirsiniz: [^fn-2]

*Main> evenSum [1..5]
accumSum 0 [1,2,3,4,5]
1 is odd
accumSum 0 [2,3,4,5]
2 is even
accumSum (0+2) [3,4,5]
3 is odd
accumSum (0+2) [4,5]
4 is even
accumSum (0+2+4) [5]
5 is odd
accumSum (0+2+4) []
l == []
0+2+4
0+6
6

İmperatif bir dilden geliyorsanız her şey doğru gözüküyor olmalı. Aslında burada pek çok şey geliştirilebilir. Öncelikle, tipi genelleyebiliriz.

evenSum :: Integral a => [a] -> a

02_Hard_Part/11_Functions.lhs


02_Hard_Part/12_Functions.lhs

Daha sonra, where veya let kullanarak alt fonksiyonlar tanımlayabiliriz. Bu şekilde accumSum fonksiyonu modülümüzün üst seviye isim uzayını (namespace) kirletmemiş olur.

-- Versiyon 2
evenSum :: Integral a => [a] -> a

evenSum l = accumSum 0 l
    where accumSum n l =
            if l == []
                then n
                else let x = head l
                         xs = tail l
                     in if even x
                            then accumSum (n+x) xs
                            else accumSum n xs

02_Hard_Part/12_Functions.lhs


02_Hard_Part/13_Functions.lhs

Sonra, örüntülü eşleme (pattern matching) kullanabiliriz.

-- Versiyon 3
evenSum l = accumSum 0 l
    where
        accumSum n [] = n
        accumSum n (x:xs) =
             if even x
                then accumSum (n+x) xs
                else accumSum n xs

Peki örüntülü eşleme nedir? Genel parametre isimleri yerine değerlerin kendisini kullanın. [^fn-3]

foo l = if l == [] then <x> else <y> demek yerine, basitçe şöyle diyorsunuz:

foo [] =  <x>
foo l  =  <y>

Ama örüntülü eşleme bundan daha fazlası. Aynı zamanda karmaşık bir değerin iç verişini takip etmenin bir yolu. Şu kodun yerine:

foo l =  let x  = head l
             xs = tail l
         in if even x
             then foo (n+x) xs
             else foo n xs

şunu yazabiliriz:

foo (x:xs) = if even x
                 then foo (n+x) xs
                 else foo n xs

Bu çok kullanışlı bir özellik. Aynı zamanda kodumuzu daha kısa ve okunaklı kılıyor.

02_Hard_Part/13_Functions.lhs


02_Hard_Part/14_Functions.lhs

Haskell'de η sadeleştirmesi yaparak fonksiyonları basitleştirebilirsiniz. Örneğin, şunu yazmak yerine:

f x = (some expression) x

basitçe şunu yazabilirsiniz:

f = some expression

Bu metodu l'yi kaldirmak icin kullanalim:

-- Version 4
evenSum :: Integral a => [a] -> a

evenSum = accumSum 0
    where
        accumSum n [] = n
        accumSum n (x:xs) =
             if even x
                then accumSum (n+x) xs
                else accumSum n xs

02_Hard_Part/14_Functions.lhs


02_Hard_Part/15_Functions.lhs

3.1.1. Üst Derece Fonksiyonlar

Higher Order

Her şeyi daha da iyi yapmak için, üst derece fonksiyonları kullanmalıyız. Peki bu canavarlar nelerdir? Üst derece fonksiyonlarlar, başka fonksiyonları parametre olarak alan fonksiyonlardır.

Bazı örnekleri şöyledir:

filter :: (a -> Bool) -> [a] -> [a]
map :: (a -> b) -> [a] -> [b]
foldl :: (a -> b -> a) -> a -> [b] -> a

Ufak adımlarla ilerleyelim.

-- Version 5
evenSum l = mysum 0 (filter even l)
    where
      mysum n [] = n
      mysum n (x:xs) = mysum (n+x) xs

ki burada,

filter even [1..10]   [2,4,6,8,10]

filter fonksiyonu a -> Bool tipinde bir fonksiyonu ve [a] tipinde bir listeyi argüman olarak alır. Bu listeden sadece bu fonksiyon çalıştırıldığında True dönen elemanları dondurur.

Sonraki adımımız, döngüye benzer bir şeyi başarmak. foldl fonksiyonunu listede adım adım ilerlerken yanda bir değer biriktirmek için kullanacağız. foldl fonksiyonu aslında şu kalıbı alıp:

myfunc list = foo initialValue list
    foo accumulated []     = accumulated
    foo tmpValue    (x:xs) = foo (bar tmpValue x) xs

Şu hale çevirir:

myfunc list = foldl bar initialValue list

Eğer gerçekten bu sihirli şeyin nasıl çalıştığını görmek istiyorsanız, foldl'in tanımı şöyledir:

foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
foldl f z [x1,...xn]
  f (... (f (f z x1) x2) ...) xn

Ama Haskell tembel olduğu için (f z x)'in değerini hesaplamaz ve sadece yığının üstüne koyar. Bu yüzden genelde foldl yerine foldl' kullanırız; foldl', foldl fonksiyonunun tembel olmayan versiyonudur. Eğer tembel ve tembel olmayan kavramlarını anlamıyorsanız, tasalanmayın, kodu foldl ve foldl' aynı şeylermiş gibi takip edin.

Şimdi evenSum fonksiyonumuzun yeni hali şöyle oldu:

-- Versiyon 6
-- foldl' dogrudan erisilebilir
-- erismek icin once Data.List modulunu iceri almamiz gerekiyor
import Data.List
evenSum l = foldl' mysum 0 (filter even l)
  where mysum acc value = acc + value

Doğrudan lambda notasyonu kullanarak daha da basitleştirebiliriz. Böylece mysum isminde geçici bir fonksiyon yaratmak zorunda kalmayız.

-- Versiyon 7
-- Genelde sadece ihtiyac duydugunuz fonksiyonlari
-- iceri almak daha iyi bir yontemdir
import Data.List (foldl')
evenSum l = foldl' (\x y -> x+y) 0 (filter even l)

Ve tabii ki, dikkat edelim ki:

(\x y -> x+y)  (+)

02_Hard_Part/15_Functions.lhs


02_Hard_Part/16_Functions.lhs

Son olarak,

-- Versiyon 8
import Data.List (foldl')
evenSum :: Integral a => [a] -> a
evenSum l = foldl' (+) 0 (filter even l)

foldl' anlaması kolay bir fonksiyon sayılmaz. Eğer alışık değilseniz, üzerinde biraz çalışmalısınız.

Burada ne olduğunu anlamanız için adım adım neler olduğuna bakalım:

  evenSum [1,2,3,4]
 foldl' (+) 0 (filter even [1,2,3,4])
 foldl' (+) 0 [2,4]
 foldl' (+) (0+2) [4]
 foldl' (+) 2 [4]
 foldl' (+) (2+4) []
 foldl' (+) 6 []
 6

Başka bir kullanışlı üst derece fonksiyon da (.) fonksiyonudur. (.) fonksiyonu matematiksel bileşimi (composition) ifade eder.

(f . g . h) x   f ( g (h x))

Bu operatörden fonksiyonumuzda η sadeleştirmesi yapmak için faydanalabiliriz:

-- Versiyon 9
import Data.List (foldl')
evenSum :: Integral a => [a] -> a
evenSum = (foldl' (+) 0) . (filter even)

Ayrıca, bazı kısımları daha iyi açıklamak için yeniden isimlendirebiliriz:

-- Versiyon 10
import Data.List (foldl')
sum' :: (Num a) => [a] -> a
sum' = foldl' (+) 0
evenSum :: Integral a => [a] -> a
evenSum = sum' . (filter even)

Şimdi bu fonksiyonel ifadelerle kodumuzun ne yöne doğru gittiğini tartışalım. Üst derece fonksiyonları kullanmak bize ne kazandırdı?

İlk önce, düşünebilirsiniz ki temel fark kısalık. Ama aslında, fark daha çok doğru düşünmeyle ilgili. Fonksiyonumuzu biraz değiştirmek istediğimizi varsayalım, örneğin bir listedeki tüm elemanların karesini alıp o çift kareleri toplamak istediğimizi.

[1,2,3,4] ▷ [1,4,9,16] ▷ [4,16] ▷ 20

Versiyon 10'u değiştirmek oldukça kolay:

squareEvenSum = sum' . (filter even) . (map (^2))
squareEvenSum' = evenSum . (map (^2))
squareEvenSum'' = sum' . (map (^2)) . (filter even)

Sadece bir tane daha transformasyon fonksiyonu ekledik, o kadar. [^fn-4]

map (^2) [1,2,3,4]  [1,4,9,16]

map fonksiyonu basitçe bir listenin tüm elemanlarını etkiler.

Fonksiyon tanımının içinde herhangi bir şey değiştirmek zorunda kalmadık. Ama ek olarak, fonksiyonunuz hakkında daha matematiksel olarak akıl yürütebiliyorsunuz. Ayrıca fonksiyonunuzu diğerleriyle değişmeli de kullanabiliyorsunuz. Yani, yeni fonksiyonunuzu kullanarak compose, map, fold, filter işlemlerini yapabilirsiniz.

Versiyon 1'i değiştirmek de okura bir alıştırma olarak kalsın. ☺.

Eğer genellemenin sonuna geldiğimizi düşünüyorsanız, oldukça yanılıyorsunuz. Örneğin, bunu sadece liste değil başka herhangi bir özyinelemeli türde kullanmanın yolları var. Eğer nasıl olduğunu bilmek istiyorsanız, size şu eğlenceli makaleyi okumanızı öneriyorum: Muz, Mercek, Zarf ve Dikenli Tellerle Fonksiyonel Programlama - Meijer, Fokkinga ve Paterson.

Bu örnek size saf fonksiyonel programlamanın ne kadar güzel olduğunu göstermeli. Ne yazık ki, saf fonksiyonel programlama her kullanıma tam uygun değil. Ya da en azından öyle bir programlama dili henüz mevcut değil.

Haskell'in büyük güçlerinden biri de alana özel dil (domain specific language) yaratma yeteneğidir, böylece programlama paradigmasını değiştirebilirsiniz.

Aslında, Haskell imperatif stilde program yazmak istediğinizde de güzeldir. İlk Haskell öğrenmeye başladığımda bunu anlamak oldukça zor olmuştu. Genelde herkes fonksiyonel yaklaşımın üstünlüğünü anlatmaya çalışır. Daha sonra Haskell'le imperatif stil kullanmaya başlayınca, nasıl ve ne zaman öyle olacağını anlamak zor olabiliyor.

Bu Haskell süper gücüyle ilgili konuşmadan önce, Haskell'in başka bir temel yönünden bahsetmeliyiz: Tipler.

02_Hard_Part/16_Functions.lhs

3.2. Tipler

Types

TL, DR (Çok uzundu okumadım):

  • type Ad = BaskaTip sadece bir takma addir ve derleyici Ad ve BaskaTip arasında bir fark gözetmez.
  • data Ad = AdYapısı BaskaTip yapısında fark vardır.
  • data anahtar kelimesi özyinelemeli yapılar yaratabilir.
  • deriving sihirlidir ve sizin için fonksiyonlar yaratır.

Haskell'de tipler güçlü ve statiktir.

Peki bu neden önemli? Çünkü bu, hatalardan kaçınmanıza yüksek derecede yardımcı olur. Haskell'de hataların çoğu henüz derleme aşamasında yakalanır. Bunun asıl sebebi de tip çıkarımının derleme sırasında yapılmasıdır. Örneğin tip çıkarımı nerede yanlış parametreyi yanlış yerde kullandığınızı yakalar.

3.2.1. Tip Çıkarımı

Statik tip sistemi hızlı çalıştırma için genelde önemlidir. Ama çoğu statik tip sistemli diller kavramları genellemede kötüdür. Haskell'in kurtarıcı lütfü, tipleri kendi kendine çıkarım yaparak bulabilmesidir.

Basit bir örnekle başlayalım, Haskell'deki square fonksiyonu:

square x = x * x

square fonksiyonu Haskell'deki herhangi bir sayısal değerin karesini alabilir. square fonksiyonuna parametre olarak Int, Integer, Float, Fractional, hatta Complex tipinde veri bile verebilirsiniz. Örnekle kanıtlayalım:

% ghci
GHCi, version 7.0.4:
...
Prelude> let square x = x*x
Prelude> square 2
4
Prelude> square 2.1
4.41
Prelude> -- load the Data.Complex module
Prelude> :m Data.Complex
Prelude Data.Complex> square (2 :+ 1)
3.0 :+ 4.0

x :+ y kompleks sayıların gösteriminde kullanılır. (x + iy)

Şimdi C'deki gerekli kod miktarıyla karşılaştıralım:

int     int_square(int x) { return x*x; }

float   float_square(float x) {return x*x; }

complex complex_square (complex z) {
    complex tmp;
    tmp.real = z.real * z.real - z.img * z.img;
    tmp.img = 2 * z.img * z.real;
}

complex x,y;
y = complex_square(x);

C'de her tip için yeni bir fonksiyon yazmanız gerekiyor. Bunu aşmanın tek yolu ön-işlemciyi kullanarak üst-programlama hilelerine başvurmak. C++'ta daha iyi bir yol var, C++ şablonları:

#include <iostream>
#include <complex>
using namespace std;

template<typename T>
T square(T x)
{
    return x*x;
}

int main() {
    // int
    int sqr_of_five = square(5);
    cout << sqr_of_five << endl;
    // double
    cout << (double)square(5.3) << endl;
    // complex
    cout << square( complex<double>(5,3) )
         << endl;
    return 0;
}

C++ bu yönden C'den çok daha iyi iş çıkartıyor. Ama daha karmaşık fonksiyonlar için söz dizimini takip etmek biraz daha zor olabilir: örnek için bu makaleye bakabilirsiniz.

C++'ta bir fonksiyonun farklı tiplerle çalışması için ayrıca belirtmelisiniz. Haskell'de durum tam tersi. Fonksiyon varsayılan olarak olabildiğince genel tanımlanır.

Tip çıkarımı Haskell'de dinamik tip sistemli dillerin yarattığı özgürlük hissini yaratır. Ama dinamik tip sistemli dillerden farklı olarak çoğu hata çalışma zamanından önce yakalanır. Genelde Haskell kodu

derleniyorsa, mutlaka kastettiğiniz şeyi yapıyordur.


02_Hard_Part/21_Types.lhs

3.2.2. Tip Oluşturma

Kendi tiplerinizi oluşturabilirsiniz. İlk önce takma adlarla, yani tip eşanlamlılarıyla başlayalım.

type Name   = String
type Color  = String

showInfos :: Name ->  Color -> String
showInfos name color =  "Isim: " ++ name
                        ++ ", Renk: " ++ color
name :: Name
name = "Ahmet"
color :: Color
color = "Mavi"
main = putStrLn $ showInfos name color

02_Hard_Part/21_Types.lhs


02_Hard_Part/22_Types.lhs

Ancak bu çok fazla koruma yaratmıyor. showInfos fonksiyonuna verdiğiniz parametrelerin yerini değiştirip çalıştırmayı deneyin:

putStrLn $ showInfos color name

Derlenecek ve çalışacak. Aslında, Name, Color ve String tiplerini birbiriyle değiştirebilirsiniz, bir fark yaratmayacak. Derleyici hepsine aynıymış gibi muamele edecek.

Diğer bir yöntem de data anahtar kelimesini kullanarak kendi tiplerinizi yaratmak.

data Name   = NameConstr String --NameConstr : isim yapicisi
data Color  = ColorConstr String --ColorConstr : renk yapicisi

showInfos :: Name ->  Color -> String
showInfos (NameConstr name) (ColorConstr color) =
      "Name: " ++ name ++ ", Color: " ++ color

name  = NameConstr "Ahmet"
color = ColorConstr "Mavi"
main = putStrLn $ showInfos name color

Ama şimdi showInfos için parametrelerin yerlerini değiştirirseniz, derleyici hata verecek! Yani bu bir daha yapmayacağınız muhtemel bir hata, ve kaçınmak için tek yapmanız gereken biraz daha uzun yazmak.

Yapıcıların da birer fonksiyon olduğuna dikkat edin:

NameConstr  :: String -> Name
ColorConstr :: String -> Color

data anahtar kelimesinin genel söz dizimi de şöyledir:

data TipAdi =   YapiciAdi  [tipler]
                | YapiciAdi2 [tipler]
                | ...

Genel kullanım tip adının ve yapıcı adının aynı olması yönündedir.

Örnek:

data Complex = Num a => Complex a a

Kayıt (record) söz dizimini de kullanabilirsiniz:

data VeriTipiAdi = VeriYapicisi {
                      alan_1 :: [alan_1 tipi]
                    , alan_2 :: [alan_2 tipi]
                    ...
                    , alan_n :: [alan_n tipi] }

Daha da iyisi, alanlara erişim sağlayan fonksiyonlar sizin için oluşturuluyor. Ayrıca bu tipte bir veri oluştururken alanların sırasını da kullanabilirsiniz.

Örnek:

data Complex = Num a => Complex { real :: a, img :: a}
c = Complex 1.0 2.0
z = Complex { real = 3, img = 4 }
real c  1.0
img z  4

02_Hard_Part/22_Types.lhs


02_Hard_Part/23_Types.lhs

3.2.3. Özyinelemeli Tipler (Recursive types)

Daha önce özyinelemeli bir tiple karşılaşmıştık: listeler. Liste tipini -biraz daha uzun bir söz dizimiyle de olsa- kendimiz de oluşturabiliriz:

data List a = Empty | Cons a (List a)

Eğer daha kolay bir söz dizimi yaratmak istiyorsanız, yapıcılar için iç notasyon (infix) tanımlayabilirsiniz.

infixr 5 :::
data List a = Nil | a ::: (List a)

infixr'dan sonraki sayı önceliği belirtiyor.

Eğer bu veri tipini ekrana yazdırmak (Show), karakter dizisinden çevirmek (Read), eşitliğini test etmek (Eq) ve karşılaştırmak (Ord) istiyorsanız, Haskell'e sizin için gerekli fonksiyonları oluşturmasını söyleyebilirsiniz.

infixr 5 :::
data List a = Nil | a ::: (List a)
              deriving (Show,Read,Eq,Ord)

Veri tipi tanımınıza deriving (Show)'u eklediğinizde, Haskell sizin için bir show fonksiyonu yaratır. ((deriving) İngilizce'de türeme demektir.) Yakında kendi show fonksiyonunuzu nasıl kullanabileceğinizi göreceğiz.

convertList [] = Nil
convertList (x:xs) = x ::: convertList xs

main = do
      print (0 ::: 1 ::: Nil)
      print (convertList [0,1])

Bu şu çıktıyı verir:

0 ::: (1 ::: Nil)
0 ::: (1 ::: Nil)

02_Hard_Part/23_Types.lhs


02_Hard_Part/30_Trees.lhs

3.2.4. Ağaçlar

Trees

Başka bir standart örnek verelim: ikili ağaçlar.

import Data.List

data BinTree a = Empty
                 | Node a (BinTree a) (BinTree a)
                              deriving (Show)

Şimdi de bir listeyi sıralı bir ikili ağaca dönüştüren bir fonksiyon yazalım:

treeFromList :: (Ord a) => [a] -> BinTree a
treeFromList [] = Empty
treeFromList (x:xs) = Node x (treeFromList (filter (<x) xs))
                             (treeFromList (filter (>x) xs))

Fonksiyonun ne kadar okunaklı olduğunu görebiliyor musunuz? Düz Türkçe olarak yazarsak:

  • boş liste, boş ağaca çevrilir.
  • bir liste (x:xs), bir ağaca çevrilir ki,
    • kok x'tır.
    • sol alt ağaç, xs'in x'ten kesin olarak küçük elemanlarından oluşturulur.
    • sağ alt ağaç, xs'in x'ten kesin olarak büyük elemanlarından oluşturulur.
main = print $ treeFromList [7,2,4,8]

Şu sonucu alıyor olmalısınız:

Node 7 (Node 2 Empty (Node 4 Empty Empty)) (Node 8 Empty Empty)

Bu ağacımızın düzgün ama zor anlaşılır bir temsili notasyonu.

02_Hard_Part/30_Trees.lhs


02_Hard_Part/31_Trees.lhs

Öylesine, ağacımız için daha iyi bir gösterim kodu yazalım. Ben genel olarak ağaçları daha iyi göstermek için bir fonksiyon yazarken eğlendim, eğer bu kısmı takip etmeyi zor buluyorsanız atlayabilirsiniz, bir sorun olmaz.

Değiştirmemiz gereken bazı şeyler var. BinTree tipımizden deriving (Show) kısmını kaldırıyoruz. Ayrıca, BinTree tipimizi (Eq ve Ord)'un sınıflarından türetmek de eşitlik ve karşılaştırma testleri yapmamızı sağlayacaktır.

data BinTree a = Empty
                 | Node a (BinTree a) (BinTree a)
                  deriving (Eq,Ord)

deriving (Show) kısmı olmadan Haskell sizin için bir show metodu yaratmaz. Biz show metodu için kendi versiyonumuzu yazacağız. Bunu başarmak için, yeni yarattığımız BinTree a'nin Show tip sınıfının bir üyesi olduğunu belirtmemiz gerekiyor. Bunun için genel söz dizimi şöyle:

instance Show (BinTree a) where
   show t = ... -- burada kendi fonksiyonunuzu tanimliyorsunuz

Benim bir ikili ağacı göstermek için yazdığım versiyon aşağıda. Karmaşıkmış gibi görünüyor ama endişelenmeyin. Daha garip nesneleri de göstermesi için bazı iyileştirmeler yaptım.

-- BinTree'a nin Show tip sinifina uye oldugunu belirtin
instance (Show a) => Show (BinTree a) where
  -- kokten once bir '<' ile baslayacagiz
  -- satir basina da : koyacagiz
  show t = "< " ++ replace '\n' "\n: " (treeshow "" t)
    where
    -- treeshow pref Tree
    --   bu fonksiyon her satira pref ile baslayarak bir agaci gosterecek
    -- Bos agaci gostermeyecek
    treeshow pref Empty = ""
    -- Yaprak
    treeshow pref (Node x Empty Empty) =
                  (pshow pref x)

    -- Sag alt agac bos
    treeshow pref (Node x left Empty) =
                  (pshow pref x) ++ "\n" ++
                  (showSon pref "`--" "   " left)

    -- Sol alt agac bos
    treeshow pref (Node x Empty right) =
                  (pshow pref x) ++ "\n" ++
                  (showSon pref "`--" "   " right)

    -- Sol ve sag alt agaclari bos olmayan agac
    treeshow pref (Node x left right) =
                  (pshow pref x) ++ "\n" ++
                  (showSon pref "|--" "|  " left) ++ "\n" ++
                  (showSon pref "`--" "   " right)

    -- Agaci guzel gostermek icin on ekler kullan
    showSon pref before next t =
                  pref ++ before ++ treeshow (pref ++ next) t

    -- pshow "\n"'i' "\n"++pref ile degistiriyor
    pshow pref x = replace '\n' ("\n"++pref) (show x)

    -- bir karakteri diger karakter dizisi ile degistiriyor
    replace c new string =
      concatMap (change c new) string
      where
          change c new x
              | x == c = new
              | otherwise = x:[] -- "x"

treeFromList metodu tamamen aynı kalıyor.

Şimdi, görelim nasıl oluyormuş:

main = do
  putStrLn "Tam sayi ikili agaci:"
  print $ treeFromList [7,2,4,8,1,3,6,21,12,23]
Tam sayi ikili agaci:
< 7
: |--2
: |  |--1
: |  `--4
: |     |--3
: |     `--6
: `--8
:    `--21
:       |--12
:       `--23

Çok daha iyi, değil mi? Ağacın kökü < karakteriyle başlayan satırda gösteriliyor. Takip eden her satır : işareti ile başlıyor. Ağacımızda başka tiplerde veri de kullanabilirdik.

  putStrLn "\nKarakter dizisi ikili agaci:"
  print $ treeFromList ["foo","bar","baz","gor","yog"]
Karakter dizisi ikili agaci:
< "foo"
: |--"bar"
: |  `--"baz"
: `--"gor"
:    `--"yog"

Ağaçların eşitliğini ve büyük/küçüklüğünü test edebildiğimiz için, ağaçlardan ağaç da yapabiliriz!

  putStrLn "\nKarakter ikili agaclarinin ikili agaci:"
  print ( treeFromList
           (map treeFromList ["baz","zara","bar"]))
Karakter ikili agaclarinin ikili agaci:
< < 'b'
: : |--'a'
: : `--'z'
: |--< 'b'
: |  : |--'a'
: |  : `--'r'
: `--< 'z'
:    : `--'a'
:    :    `--'r'

Ağacın her satırını bu yüzden : ile başlatmıştım. (kök hariç)

Yo

  putStrLn "\nKarakter ikili agaclarinin ikili agaci:"
  print $ (treeFromList . map (treeFromList . map treeFromList))
             [ ["YO","DAWG"]
             , ["I","HEARD"]
             , ["I","HEARD"]
             , ["YOU","LIKE","TREES"] ]

ki bu da şuna denk:

print ( treeFromList (
          map treeFromList
             [ map treeFromList ["YO","DAWG"]
             , map treeFromList ["I","HEARD"]
             , map treeFromList ["I","HEARD"]
             , map treeFromList ["YOU","LIKE","TREES"] ]))

ve şu çıktıyı vermeli:

Karakter ikili agaclarinin ikili agaci:
< < < 'Y'
: : : `--'O'
: : `--< 'D'
: :    : |--'A'
: :    : `--'W'
: :    :    `--'G'
: |--< < 'I'
: |  : `--< 'H'
: |  :    : |--'E'
: |  :    : |  `--'A'
: |  :    : |     `--'D'
: |  :    : `--'R'
: `--< < 'Y'
:    : : `--'O'
:    : :    `--'U'
:    : `--< 'L'
:    :    : `--'I'
:    :    :    |--'E'
:    :    :    `--'K'
:    :    `--< 'T'
:    :       : `--'R'
:    :       :    |--'E'
:    :       :    `--'S'

Tekrar edilen ağaçların eklenmediğine dikkat edin; "I","HEARD"'e denk gelen sadece bir ağaç var. Bunun için (neredeyse) hiçbir şey yapmadık, çünkü Tree yapısını Eq tip sınıfından türettik.

Bu yapının ne kadar müthiş olduğunu görebiliyor musunuz: Sadece sayılardan, karakter dizilerinden, karakterlerden değil, başka ağaçlardan da ağaçlar yapabiliyoruz. İstersek ağaçlardan oluşan ağaçlardan oluşan ağaç bile yapabiliriz!

02_Hard_Part/31_Trees.lhs


02_Hard_Part/40_Infinites_Structures.lhs

3.3. Sonsuz Yapılar

Infinite

Haskell'in tembel olduğu sıkça söylenir.

Aslında, eğer biraz titizseniz, Haskell'in non-strict (aceleci olmayan, kesin olmayan) olduğunu söylemelisiniz. Tembellik sadece non-strict dillerin ortak bir tatbikidir.

Öyleyse "non-strict" tam olarak ne anlama geliyor? Haskell vikisiden alıntılayalım:

Sadeleştirme (hesaplama için matematiksel terim) dıştan içe doğru ilerler.

yani eğer (a+(b*c))'yi ele alıyorsanız, önce +'yi sadeleştirirsiniz, sonra iç (b*c)'yi sadeleştirirsiniz.

Örneğin Haskell'de şunu yapabilirsiniz:

-- numbers = [1,2,..]
numbers :: [Integer]
numbers = 0:map (1+) numbers

take' n [] = []
take' 0 l = []
take' n (x:xs) = x:take' (n-1) xs

main = print $ take' 10 numbers

Sonra duruyor.

Neden?

numbers değişkeninin tamamını hesaplamak yerine, sadece ihtiyacı olan elemanları, ihtiyacı olduğu zaman hesaplıyor.

Ayrıca, Haskell'de sonsuz listeler için bir notasyon olduğunu da söylemiş olayım:

[1..]    [1,2,3,4...]
[1,3..]  [1,3,5,7,9,11...]

ve çoğu fonksiyon da onlarla çalışır. Ayrıca, Haskell'de, bizim take' fonksiyonumuza denk bir take fonksiyonu mevcuttur.

02_Hard_Part/40_Infinites_Structures.lhs


02_Hard_Part/41_Infinites_Structures.lhs

Sıralı ikili ağaç yaptığımızı varsayalım. Sonsuz bir ikili ağaç şöyle olur:

nullTree = Node 0 nullTree nullTree

Her düğümün 0'a eşit olduğu, geçerli ve tam bir ikili ağaç. Şimdi bu objeyi aşağıdaki fonksiyonla işleyebileceğimi kanıtlayacağım:

-- bir BinTree'nin tum elemanlarini
-- belli bir derinlige kadar al
treeTakeDepth _ Empty = Empty
treeTakeDepth 0 _     = Empty
treeTakeDepth n (Node x left right) = let
          nl = treeTakeDepth (n-1) left
          nr = treeTakeDepth (n-1) right
          in
              Node x nl nr

Bu programın sonucunu görelim.

main = print $ treeTakeDepth 4 nullTree

Kodumuz derleniyor, çalışıyor ve şu sonucu vererek duruyor:

<  0
: |-- 0
: |  |-- 0
: |  |  |-- 0
: |  |  `-- 0
: |  `-- 0
: |     |-- 0
: |     `-- 0
: `-- 0
:    |-- 0
:    |  |-- 0
:    |  `-- 0
:    `-- 0
:       |-- 0
:       `-- 0

Nöronlarınızı biraz daha ısındırmak için daha ilginç bir ağaca bakalım:

iTree = Node 0 (dec iTree) (inc iTree)
        where
           dec (Node x l r) = Node (x-1) (dec l) (dec r)
           inc (Node x l r) = Node (x+1) (inc l) (inc r)

Bu ağacı oluşturmanın başka bir yolu da üst derece fonksiyonları kullanmaktır. Bu fonksiyon map fonksiyonuna benziyor, ama listeler yerine BinTree'ler üzerinde çalışıyor. Ortaya şöyle bir fonksiyon çıkacak:

-- bir fonksiyonu agacin her dugumune uygular
treeMap :: (a -> b) -> BinTree a -> BinTree b
treeMap f Empty = Empty
treeMap f (Node x left right) = Node (f x)
                                     (treeMap f left)
                                     (treeMap f right)

Not: Bunun hakkında burada daha fazla konuşmayacağım. Eğer map'in diğer veri yapılarına genellemesiyle ilgileniyorsanız, functorları ve fmap'i araştırın.

Şimdi tanımımız şöyle oldu:

infTreeTwo :: BinTree Int
infTreeTwo = Node 0 (treeMap (\x -> x-1) infTreeTwo)
                    (treeMap (\x -> x+1) infTreeTwo)

Şunun sonucuna bakalım:

main = print $ treeTakeDepth 4 infTreeTwo
<  0
: |-- -1
: |  |-- -2
: |  |  |-- -3
: |  |  `-- -1
: |  `-- 0
: |     |-- -1
: |     `-- 1
: `-- 1
:    |-- 0
:    |  |-- -1
:    |  `-- 1
:    `-- 2
:       |-- 1
:       `-- 3

02_Hard_Part/41_Infinites_Structures.lhs

4. Çok Zor Kısım

Buraya kadar geldiyseniz tebrikler! Şimdi gerçekten çok zor kısım başlayabilir.

Eğer benim gibiyseniz, fonksiyonel stili anlamış olmalısınız. Ayrıca tembelliğin varsayılan olmasının avantajlarını da biraz anlamış olmalısınız. Ama gerçek bir program yapmaya nereden başlamanız gerektiğini bilmiyorsunuz. Özellikle de şu soruların cevaplarını:

  • Yan etkilerle nasıl baş edilir?
  • Neden IO (girdi-çıktı) ile baş etmek için imperatifliğe benzer bir notasyon var?

Karmaşık cevaplara hazır olun. Ama hepsi sonunda çok faydalı.


03_Hell/01_IO/01_progressive_io_example.lhs

4.1. IO ile Baş Etmek

IO

TL;DR: (Çok uzundu okumadım)

IO ile uğraşan tipik bir fonksiyon imperatif bir programa çok benzer:

f :: IO a
f = do
  x <- action1
  action2 x
  y <- action3
  action4 x y
  • Bir nesnenin değerini belirtmek için <- kullanıyoruz.
  • Her satırın tipi IO *, bu örnekte:
  • action1 :: IO b
  • action2 x :: IO ()
  • action3 :: IO c
  • action4 x y :: IO a
  • x :: b, y :: c
  • Az sayıda nesnenin tipi IO a'dir, bu seçmenize yardım eder. Farklı olarak, burada saf fonksiyonları doğrudan kullanamazsınız. Saf fonksiyonları kullanmak için örneğin action2 (saffonksiyon x) yazabilirsiniz.

Bu bölümde size IO kullanmayı anlatacağım, ama nasıl çalıştığını değil. Haskell'in nasıl saf ve saf olmayan kısımları ayırdığını göreceksiniz.

Söz dizimindeki detayları anlamaya çalışmak için durmayın. Cevaplar ilerleyen bölümde gelecek.

Ne yapalım?

Kullanıcıdan bir sayı listesi girmesini isteyin. Sayıların toplamını ekrana yazdırın.

toList :: String -> [Integer]
toList input = read ("[" ++ input ++ "]")

main = do
  putStrLn "Bir sayi listesi girin (virgulle ayirin):"
  input <- getLine
  print $ sum (toList input)

Bu programın ne yaptığı oldukça açık olmalı. Tiplere biraz daha ayrıntılı bakalım.

putStrLn :: String -> IO ()
getLine  :: IO String
print    :: Show a => a -> IO ()

Daha da ilginç şekilde, do bloğunun içindeki her ifadenin IO a tipinde olduğuna dikkat edelim.

main = do
  putStrLn "Enter ... " :: IO ()
  getLine               :: IO String
  print Something       :: IO ()

Ayrıca <- işaretinin etkisine de dikkat edelim.

do
 x <- something -- bir seyler

Eğer something :: IO a tipinde ise x :: a'dir.

IO kullanımıyla ilgili başka bir önemli nokta da şudur: do bloğunun içindeki tüm satırlar şu iki şekilden birinde olmalı:

action1             :: IO a
                    -- bu durumda, a = ()

veya

deger <- action2    -- ki burada
                    -- bar z t :: IO b
                    -- deger   :: b

Bu iki çeşit komut aksiyonları sıralamanın iki farklı yolunu ifade ediyor. Bu cümlenin anlamı önümüzdeki bölümün sonunda netleşecek.

03_Hell/01_IO/01_progressive_io_example.lhs


03_Hell/01_IO/02_progressive_io_example.lhs

Şimdi programımızın nasıl davrandığına bakalım. Örneğin, eğer kullanıcı garip bir şey girerse ne olacak? Deneyelim:

    % runghc 02_progressive_io_example.lhs
    Enter a list of numbers (separated by comma):
    foo
    Prelude.read: no parse

Püf! Garip bir hata mesajından sonra programımız çöktü! İlk iyileştirmemiz daha anlaşılır bir hata mesajı vermek olsun.

Bunu yapmak için önce bir şeylerin yanlış gittiğini tespit edebilmemiz gerekiyor. İşte bunu yapmanın bir yolu: Maybe (Türkçesi: belki) tipini kullanmak. Bu Haskell'de sık kullanılan bir tiptir.

import Data.Maybe

Peki bu nedir ki? Maybe bir parametre alan bir tiptir. Tanımı da şudur:

data Maybe a = Nothing | Just a

Bu bir değer okumaya veya yaratmaya çalışırken bir hata olduğunu ifade etmenin güzel bir yoludur. maybeRead fonksiyonu bunun iyi bir örneği. Bu read'e [^fn-5] benzer bir fonksiyon, ama eğer bir şeyler yanlış giderse dönen değer Nothing olacak. Eğer bir değer okuyabilirse, dönen değer Just <değer> olacak. Bu fonksiyonu çok anlamaya çalışmayın; read'den daha alt seviye bir fonksiyon olan reads'i kullanıyorum.

maybeRead :: Read a => String -> Maybe a
maybeRead s = case reads s of
                  [(x,"")]    -> Just x
                  _           -> Nothing

Biraz daha okunaklı olması için, şu şekilde giden bir fonksiyon tanımlayalım: Eğer karakter dizisinin formatı yanlışsa, Nothing döndürülecek. Aksi halde, örneğin "1,2,3" için Just [1,2,3] döndürülecek.

getListFromString :: String -> Maybe [Integer]
getListFromString str = maybeRead $ "[" ++ str ++ "]"

Sonrasında tek yapmamız gereken dönen değeri ana fonksiyonumuzda test etmek.

main :: IO ()
main = do
  putStrLn "Bir sayi listesi girin (virgulle ayirin):"
  input <- getLine
  let maybeList = getListFromString input in
      case maybeList of
          Just l  -> print (sum l)
          Nothing -> error "Kotu format. Hoscakalin."

Hata durumunda, iyi sayılabilecek bir hata mesajı göstermiş olalım.

main fonksiyonunun do bloğundaki her ifadenin tipinin IO a formatında olduğuna dikkat edin. Tek bilmediğiniz yapı error. Ama error msg de gerekli tipi alıyor. (burada IO ()).

Bu programda fark etmeniz gereken şey tanımladığımız fonksiyonların tipleri. Yazdığımız fonksiyonlar arasında sadece bir tanesinin tipinde IO var: main. Bu demek oluyor ki main saf olmayan bir fonksiyon. Ama main içinde saf bir fonksiyon olan getListFromString kullanılıyor. Yani sadece bakarak bile fonksiyonların saf olup olmadıklarını anlayabilirsiniz.

Peki saflık neden önemlidir? Bir sürü avantajının üçü şunlar:

  • Saf kod hakkında mantık yürütmek saf olmayan kod hakkında mantık yürütmekten çok daha kolaydır.
  • Saflık sizi yan etkilerden ötürü kolayca test edemeyeceğiniz hatalardan korur.
  • Saf fonksiyonları herhangi bir sırayla veya eşzamanlı olarak hiçbir risk olmadan hesaplayabilirsiniz.

İşte bu yüzden, olabildiğince fazla kodunuzu saf fonksiyonlarla yazmalısınız.

03_Hell/01_IO/02_progressive_io_example.lhs


03_Hell/01_IO/03_progressive_io_example.lhs

Sonraki adımımız kullanıcı geçerli bir cevap girene kadar tekrar tekrar sormak olsun.

İlk bölümü aynen kullanabiliriz:

import Data.Maybe

maybeRead :: Read a => String -> Maybe a
maybeRead s = case reads s of
                  [(x,"")]    -> Just x
                  _           -> Nothing
getListFromString :: String -> Maybe [Integer]
getListFromString str = maybeRead $ "[" ++ str ++ "]"

Şimdi kullanıcı doğru bir girdi yazana kadar tekrar soran bir fonksiyon yazalım:

askUser :: IO [Integer]
askUser = do
  putStrLn "Bir sayi listesi girin (virgulle ayirin):"
  input <- getLine
  let maybeList = getListFromString input in
      case maybeList of
          Just l  -> return l
          Nothing -> askUser

Fonksiyonumuzun tipi IO [Integer]. Bu demek oluyor ki belirli IO aksiyonları sonucu [Integer] tipinde bir değer elde ediyoruz. Bazıları bunu şöyle açıklıyor:

«IO içinde [Integer] var.»

Eğer bunun arkasındaki detayları anlamak istiyorsanız, sonraki bölümü okumanız gerekecek. Ama eğer IO'yu sadece kullanmak istiyorsanız, biraz tekrar yapın ve tipler hakkında düşünmeniz gerektiğini hatırlayın.

Son olarak, main fonksiyonumuz çok daha basit:

main :: IO ()
main = do
  list <- askUser
  print $ sum list

IO'ya girişimizi bitirdik. Biraz hızlıydı, değil mi? Hatırlamamız gereken temel şeyler sunlar:

  • do bloğunun içinde, her ifade IO a tipinde olmalı. Bu sizi belli ifadelerle kısıtlıyor. Örneğin, getLine, print, putStrLn, vs.
  • Saf fonksiyonları olabildiğince saf olmayan kısımların dışında tutmaya çalışın, işin mümkün olduğunca büyük kısmını saf fonksiyonlara yaptırın.
  • IO a, a tipinde bir eleman döndüren IO aksiyonu demektir. IO aksiyonu temsil eder, IO a aslında bir fonksiyonun tipidir. Daha fazlasını merak ediyorsanız sonraki bölümü okuyun.

Biraz çalışırsanız, IO kullanabiliyor olmalısınız.

Alıştırma: Tüm argümanlarının toplamını alan bir program yazın. İpucu: getArgs fonksiyonunu kullanın.

03_Hell/01_IO/03_progressive_io_example.lhs

4.2. IO'nun Püf Noktası

Bu bir pipo degildir

Bu bölüm için TL; DR (Çok uzundu okumadım)

Saf ve saf olmayan kısımları ayırmak için, main dünyanın durumunu değiştiren bir fonksiyon olarak tanımlanır.

main :: World -> World

Bir fonksiyon, sadece ve sadece bu tipe sahipse yan etkide bulunur. Tipik bir main fonksiyonuna bakalım:

main w0 =
    let (v1,w1) = action1 w0 in
    let (v2,w2) = action2 v1 w1 in
    let (v3,w3) = action3 v2 w2 in
    action4 v3 w3

Sonraki aksiyona aktarmamız gereken bir sürü geçici elemanımız var. (burada w1, w2 ve w3)

bind veya (>>=) fonksiyonu yaratıyoruz. bind ile artık geçici isimlere ihtiyacımız yok.

main =
  action1 >>= action2 >>= action3 >>= action4

Bonus: Haskell'in şöyle bir söz dizimsel kolaylığı var:

main = do
  v1 <- action1
  v2 <- action2 v1
  v3 <- action3 v2
  action4 v3

Neden böyle garip bir söz dizimi kullandık ve bu IO tipi tam olarak nedir? Anlaşılmaz duruyor ama, değil.

Şimdilik, programımızın saf kısımlarını bir kenara bırakıp saf olmayan kısımlar üzerinde yoğunlaşalım:

askUser :: IO [Integer]
askUser = do
  putStrLn "Bir sayi listesi girin (virgulle ayirin):"
  input <- getLine
  let maybeList = getListFromString input in
      case maybeList of
          Just l  -> return l
          Nothing -> askUser

main :: IO ()
main = do
  list <- askUser
  print $ sum list

İlk tepki: İmperatif duruyor. Haskell saf olmayan kodu imperatif gösterecek kadar güçlüdür. Örneğin, isterseniz Haskell'de while yapısı yaratabilirsiniz. Hatta, IO ile uğraşman için imperatif stip genellikle daha uygundur.

Ama notasyonun alışılmışın dışında olduğunu fark etmiş olmalısınız. Şimdi bunun sebeplerini detaylıca tartışalım.

Saf olmayan bir dilde, dünyanın durumu devasa ve gizli bir global değişken olarak görülebilir. Bu gizli değişkene dildeki tüm fonksiyonlar tarafından erişilebilir. Örneğin herhangi bir fonksiyon içinde bir dosya ile okuma/yazma işlemleri yapabilirsiniz. Dosyanın var olup olmaması "dünya"nızın alabileceği olası durumlardır.

Haskell'de bu durum gizli değildir. Tam tersine, Haskell'de main'in dünyanızın durumunu değiştirme olasılığı olduğu özellikle ayrıca söylenir. Bu fonksiyonun tipi şunun gibi bir şeydir:

main :: World -> World

Bu değişkene tüm fonksiyonların erişimi yoktur. Erişimi olan fonksiyonlar saf değildir. Dünya değişkenine erişimi olmayan fonksiyonlar ise saftır. [^fn-6]

Haskell, dünyanın durumunu main fonksiyonuna bir girdi değişkeni olarak görür. Ama main'in asıl tipi suna daha yakındır: [^fn-7]

main :: World -> ((),World)

() tipi birim tipidir. Burda görülecek bir şey yok.

Şimdi bunu aklımızda tutarak main fonksiyonumuzu baştan yazalım:

main w0 =
    let (list,w1) = askUser w0 in
    let (x,w2) = print (sum list,w1) in
    x

İlk olarak, yan etkisi olan tüm fonksiyonların şu tipte olması gerektiğini hatırlayalım:

World -> (a,World)

Burada a sonucun tipi oluyor. Örneğin getChar fonksiyonu bu durumda World -> (Char,World) tipindedir.

Dikkat edilmesi gereken diğer bir şey ise hesaplama/değerlendirme sırası. Örneğin f a b'yi hesaplarken birden fazla seçeneğiniz var:

  • önce a'yi, sonra b'yi, sonra da f a b'yi hesapla.
  • önce b'yi, sonra a'yi, sonra da f a b'yi hesapla.
  • a'yi ve b'yi paralel olarak, sonra da f a b'yi hesapla.

Bu böyle çünkü dilin saf kısmında çalışıyoruz.

Şimdi, eğer main fonksiyonuna bakarsanız, ilk satırı ikinci satırdan önce hesaplamanız gerektiği açık, çünkü ikinci satırda birinci satırda elde ettiğiniz bir değeri parametre olarak kullanıyorsunuz.

Bu işe yarıyor. Derleyici her adımda yeni bir gerçek dünya tanımı/kodu (id) sağlıyor. Gizli olarak, print şöyle işliyor:

  • ekrana bir şey yazdır
  • dünyanın id'sini değiştir.
  • ((), yeni dünya id'si) değerini döndür.

Şimdi, eğer main fonksiyonunun stiline bakarsanız, biraz garip olduğunu göreceksiniz. Aynısını askUser fonksiyonuna da uygulayalım:

askUser :: World -> ([Integer],World)

Öncesi:

askUser :: IO [Integer]
askUser = do
  putStrLn "Bir sayi listesi girin:"
  input <- getLine
  let maybeList = getListFromString input in
      case maybeList of
          Just l  -> return l
          Nothing -> askUser

Sonrası:

askUser w0 =
    let (_,w1)     = putStrLn "Bir sayi listesi girin:" in
    let (input,w2) = getLine w1 in
    let (l,w3)     = case getListFromString input of
                      Just l   -> (l,w2)
                      Nothing  -> askUser w2
    in
        (l,w3)

Benzer, ama biraz daha garip. Tüm şu geçici w?'lere bakın.

Çıkarılacak ders şu: Saf fonksiyonel dillerdeki naif IO uygulamaları gariptir!

Şanslıyız ki, bu sorunu halletmek için daha iyi bir yol var. Bir kalip goruyoruz, her satir su sekilde:

let (y,w') = action x w in

Herhangi bir satır için ilk x argümanı gerekmese bile. Dönen sonucun tipi bir ikili, (sonuç, yeniDünyaDeğeri). Her f fonksiyonu şuna benzer bir tipe sahip olmak zorunda:

f :: World -> (a,World)

Her zaman aynı kalıbı takip ettiğimize dikkat edin:

let (y,w1) = action1 w0 in
let (z,w2) = action2 w1 in
let (t,w3) = action3 w2 in
...

Her aksiyon 0'dan n'ye kadar parametre alabilir. Ve özellikle, her aksiyon bir üstündeki satırın sonucundan parametre alabilir.

Örneğin, şöyle diyebiliriz:

let (_,w1) = action1 x w0   in
let (z,w2) = action2 w1     in
let (_,w3) = action3 x z w2 in
...

Ve tabii ki actionN w :: (World) -> (a,World).

Önemli: Dikkat edilmesi gereken iki kalıp var:

let (x,w1) = action1 w0 in
let (y,w2) = action2 x w1 in

ve

let (_,w1) = action1 w0 in
let (y,w2) = action2 w1 in

Joker

Şimdi biraz sihirbazlık yapalım. Geçici dünya sembolünü "yok edelim". İki satırı bind ile birbirine bağlayacağız. İşe bind fonksiyonunu tanımlayarak başlayalım. Fonksiyonun tipi başta biraz korkutucu gelebilir:

bind :: (World -> (a,World))
        -> (a -> (World -> (b,World)))
        -> (World -> (b,World))

Ama hatırlayın ki (World -> (a,World)) IO aksiyonlarının tipidir. Daha açık olması için ona yeni bir isim verelim:

type IO a = World -> (a, World)

Bazı fonksiyon örnekleri:

getLine :: IO String
print :: Show a => a -> IO ()

getLine dünyayı parametre olarak alan ve (String,World) ikilisini döndüren bir IO aksiyonudur. Bu şöyle özetlenebilir: getLine, IO String tipindedır, ki bunu "IO içine gömülmüş" String döndüren bir IO aksiyonu olarak görebiliriz.

print fonksiyonu da ayrıca ilginçtir. Gösterilebilir bir argüman alır, ama aslında iki argüman almaktadır. İlki ekrana yazdırılacak değerdir, ikincisi de dünyanın durumudur. Sonrasında ise ((),World) ikilisini döndürür. Bu fonksiyonun dünyanın durumunu değiştirdiği ama başka bir veri üretmediği anlamına gelir.

Bu tip, bind fonksiyonun tipini basitleştirmemize yardımcı olur:

bind :: IO a
        -> (a -> IO b)
        -> IO b

Bu demek oluyor ki bind fonksiyonu iki IO aksiyonunu parametre olarak alıyor ve başka bir IO aksiyonunu geri döndürüyor.

Önemli kalıpları tekrar hatırlayın. İlki şuydu:

let (x,w1) = action1 w0 in
let (y,w2) = action2 x w1 in
(y,w2)

Fonksiyonların tiplerine bakalım:

action1  :: IO a
action2  :: a -> IO b
(y,w2)   :: IO b

Tanıdık gelmiyor mu?

(bind action1 action2) w0 =
    let (x, w1) = action1 w0
        (y, w2) = action2 x w1
    in  (y, w2)

Amacımız World argümanını bu fonksiyonla gizlemek. Örnek olarak, şunu gerçekleştirmek istediğimizi varsayalım:

let (line1,w1) = getLine w0 in
let ((),w2) = print line1 in
((),w2)

Şimdi, bind fonksiyonunu kullanarak:

(res,w2) = (bind getLine (\l -> print l)) w0

print fonksiyonu (World -> ((),World)) tipinde olduğu için, res = () (boş tip) Eğer bundaki sihirbazlığı görmediyseniz, bu sefer üç satırla deneyelim:

let (line1,w1) = getLine w0 in
let (line2,w2) = getLine w1 in
let ((),w3) = print (line1 ++ line2) in
((),w3)

Bu da şuna denk:

(res,w3) = (bind getLine (\line1 ->
             (bind getLine (\line2 ->
               print (line1 ++ line2))))) w0

Bir şey fark ettiniz mi? Evet, artık hiçbir yerde geçici World değişkenleri kullanılmıyor. Sihirbazlık gibi.

Daha iyi bir notasyon kullanabiliriz. bind yerine (>>=) kullanalım. (>>=), (+) gibi bir iç notasyonlu fonksiyondur; ki şöyle çalışır: 3 + 4 ⇔ (+) 3 4

Ho Ho Ho! Herkese Mutlu Noeller! Haskell bize söz dizimsel kolaylık sağlıyor:

do
  x <- action1
  y <- action2
  z <- action3
  ...

yerine, şöyle yazabiliriz:

action1 >>= (\x ->
action2 >>= (\y ->
action3 >>= (\z ->
...
)))

action2 içinde x, action3 içinde hem x hem de y değişkenlerini kullanabildiğinize dikkat edin.

Peki <- kullanmayan satırlarda ne yapacağız? Kolay, blindBind diye başka bir fonksiyon tanımlayalım:

blindBind :: IO a -> IO b -> IO b
blindBind action1 action2 w0 =
    bind action (\_ -> action2) w0

Bu tanımı daha açık olması için basitleştirmedim. Tabii daha basit bir notasyon kullanabiliriz, bunun için (>>) operatörü var.

do
    action1
    action2
    action3

Şuna dönüşüyor:

action1 >>
action2 >>
action3

Kullanışlı bir fonksiyon daha var.

putInIO :: a -> IO a
putInIO x = IO (\w -> (x,w))

Bu saf değerleri IO bağlamına sokmak için kullanılan genel bir yoldur. putInIO için kullanılan genel isim return'dür. Haskell öğrenirken bu oldukça kötü bir isim, çünkü Haskell'deki return alışık olduğunuzden çok farklıdır.

03_Hell/01_IO/21_Detailled_IO.lhs

Örneğimizi çevirerek bu bölümü bitirelim:

askUser :: IO [Integer]
askUser = do
  putStrLn "Bir sayi listesi girin (virgullerle ayirin):"
  input <- getLine
  let maybeList = getListFromString input in
      case maybeList of
          Just l  -> return l
          Nothing -> askUser

main :: IO ()
main = do
  list <- askUser
  print $ sum list

Şu hale geliyor:

import Data.Maybe

maybeRead :: Read a => String -> Maybe a
maybeRead s = case reads s of
                  [(x,"")]    -> Just x
                  _           -> Nothing
getListFromString :: String -> Maybe [Integer]
getListFromString str = maybeRead $ "[" ++ str ++ "]"
askUser :: IO [Integer]
askUser =
    putStrLn "Bir sayi listesi girin (virgullerle ayirin):" >>
    getLine >>= \input ->
    let maybeList = getListFromString input in
      case maybeList of
        Just l -> return l
        Nothing -> askUser

main :: IO ()
main = askUser >>=
  \list -> print $ sum list

Çalıştığını doğrulamak için derleyebilirsiniz.

(>>) ve (>>=) olmadan nasıl olacağını düşünün.

03_Hell/01_IO/21_Detailled_IO.lhs


03_Hell/02_Monads/10_Monads.lhs

4.3. Monad

Monad

Artık bu sırrı açıklayabiliriz: IO bir monaddir. Monad olmak, do notasyonu ile belirli söz dizimsel kolaylıklara sahip olmak demektir. Ama temel olarak, kodunuzun akışını kolaylaştıracak belirli kod kalıplarına erişim sağlar.

Önemli noktalar:

  • Monadlar etkilerle ilgili olmak zorunda değildir! Saf monadlar da vardır.
  • Monadlar daha çok sıralama ile ilgilidir.

Haskell'de Monad bir tip sınıfıdır. Bu sınıfın bir üyesi olmak için (>>=) ve return fonksiyonlarını sağlamalısınız. (>>) fonksiyonu (>>=) fonksiyonundan türer. Basit olarak Monad tip sınıfı şöyle belirtilir:

class Monad m  where
  (>>=) :: m a -> (a -> m b) -> m b
  return :: a -> m a

  (>>) :: m a -> m b -> m b
  f >> g = f >>= \_ -> g

  -- Sadece tarihsel sebeplerden dolayi oldugunu dusundugum
  -- bu fonksiyonu dikkate almayabilirsiniz
  fail :: String -> m a
  fail = error

Notlar:

  • class anahtar kelimesi dostunuz değildir. Haskell'deki class nesne yönelimli programlamada karşılaştığınız class gibi değildir. Haskell'deki class Java'daki interface'le benzeşir. typeclass daha iyi bir isimlendirme olurdu, çünkü o tip grubu anlamına geliyor. Bir tipin bir sınıfa ait olması için, bir sınıfın tüm fonksiyonlarının o tip için sağlanabilir olması gerekiyor.
  • Tip sınıflarının bu örneğinde, m tipinin argüman alan bir tip olması gerekiyor, örneğin IO a, ama aynı zamanda Maybe a, [a], vs.
  • Fonksiyonunuzun kullanışlı bir monad olması için bazı kurallara uyması gerekiyor. Eğer yapınız bu kurallara uymuyorsa garip şeyler gerçekleşebilir: ~ return a >>= k == k a m >>= return == m m >>= (-> k x >>= h) == (m >>= k) >>= h ~

4.3.1 Maybe Monad'ı

Monad tip sınıfının üyesi olan bir sürü farklı tip vardır. Tarif etmesi en kolay olanlarından biri de Maybe'dir. Eğer Maybe değerlerinden oluşan bir diziniz varsa, etkilemek için monadları kullanabilirsiniz. Uzayıp giden ıf..then..else.. yapılarından kurtulmak için oldukça kullanışlı bir yoldur.

Karmaşık bir banka işlemi düşünün. Eğer bakiyeniz sıfırın altına düşmeden belli işlemleri yapabilecek kadar paranız varsa, 700€ kazanma hakkınız olsun.

deposit  value account = account + value --para yatirma
withdraw value account = account - value --para cekme

--para kazanmaya hak kazanip kazanmadiginizi donduren fonksiyon
eligible :: (Num a,Ord a) => a -> Bool
eligible account =
  let account1 = deposit 100 account in
    if (account1 < 0)
    then False
    else
      let account2 = withdraw 200 account1 in
      if (account2 < 0)
      then False
      else
        let account3 = deposit 100 account2 in
        if (account3 < 0)
        then False
        else
          let account4 = withdraw 300 account3 in
          if (account4 < 0)
          then False
          else
            let account5 = deposit 1000 account4 in
            if (account5 < 0)
            then False
            else
              True

main = do
  print $ eligible 300 -- True
  print $ eligible 299 -- False

03_Hell/02_Monads/10_Monads.lhs


03_Hell/02_Monads/11_Monads.lhs

Şimdi Maybe kullanarak ve monadlardan faydalanarak iyileştirelim:

deposit :: (Num a) => a -> a -> Maybe a
deposit value account = Just (account + value)

withdraw :: (Num a,Ord a) => a -> a -> Maybe a
withdraw value account = if (account < value)
                         then Nothing
                         else Just (account - value)

eligible :: (Num a, Ord a) => a -> Maybe Bool
eligible account = do
  account1 <- deposit 100 account
  account2 <- withdraw 200 account1
  account3 <- deposit 100 account2
  account4 <- withdraw 300 account3
  account5 <- deposit 1000 account4
  Just True

main = do
  print $ eligible 300 -- Just True
  print $ eligible 299 -- Nothing

03_Hell/02_Monads/11_Monads.lhs


03_Hell/02_Monads/12_Monads.lhs

Kötü değil, ama daha da iyileştirebiliriz:

deposit :: (Num a) => a -> a -> Maybe a
deposit value account = Just (account + value)

withdraw :: (Num a,Ord a) => a -> a -> Maybe a
withdraw value account = if (account < value)
                         then Nothing
                         else Just (account - value)

eligible :: (Num a, Ord a) => a -> Maybe Bool
eligible account =
  deposit 100 account >>=
  withdraw 200 >>=
  deposit 100  >>=
  withdraw 300 >>=
  deposit 1000 >>
  return True

main = do
  print $ eligible 300 -- Just True
  print $ eligible 299 -- Nothing

Monadların kodumuzu daha zarifleştirmenin iyi bir yolu olduğunu gösterdik. Dikkat edin ki, bu tip bir kod düzenlemesi, özellikle Maybe, pek çok imperatif dilde de uygulanabilir. Aslında, bu tip bir yapıyı normalde de yapıyoruz.

Önemli bir nokta: Dizideki ilk elemanın Nothing olarak hesaplanması tüm hesaplamayı durduracaktır. Bu demek oluyor ki, tüm satırları hesaplamıyorsunuz. Tembellik sayesinde bunun için fazladan bir şey yapmanıza gerek yok.

Bunu göz önünde bulundurarak Maybe için (>>=) fonksiyonunu şöyle tanımlayabilirsiniz:

instance Monad Maybe where
    (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
    Nothing  >>= _  = Nothing
    (Just x) >>= f  = f x

    return x = Just x

Bu basit örnek ile Maybe monadının ne kadar kullanışlı olabileceğini ve nasıl kullanılabileceğini gördük. Ama daha iyi bir örnek için listelere bakalım.

03_Hell/02_Monads/12_Monads.lhs


03_Hell/02_Monads/13_Monads.lhs

4.3.2. Liste Monad'ı

liste

Liste monadı deterministik (belirlenimci) olmayan hesaplamaları simüle etmemize yardımcı olur. Şöyle:

import Control.Monad (guard)

allCases = [1..10]

resolve :: [(Int,Int,Int)]
resolve = do
              x <- allCases
              y <- allCases
              z <- allCases
              guard $ 4*x + 2*y < z
              return (x,y,z)

main = do
  print resolve

Resmen sihirbazlık.

[(1,1,7),(1,1,8),(1,1,9),(1,1,10),(1,2,9),(1,2,10)]

Liste monadı için, şöyle bir söz dizimsel kolaylık da vardır:

  print $ [ (x,y,z) | x <- allCases,
                      y <- allCases,
                      z <- allCases,
                      4*x + 2*y < z ]

Tüm monadları sıralamayacağım, ancak bir sürü monad bulunmakta. Saf dillerdeki belli kavramlar monad kullanımı ile basitleştirilebilir. Özellikle monadlar şunlar için kullanışlıdır:

  • IO
  • deterministik olmayan hesaplamalar
  • (sözde) rastgele sayı üretimi
  • konfigürasyon durumunu tutma
  • yazma durumu
  • ..

Eğer buraya kadar beni takip edebildiyseniz, başardınız! Monadları öğrendiniz! [^fn-8]

03_Hell/02_Monads/13_Monads.lhs

5. Ekler

Bu bölüm doğrudan Haskell'le ilgili değil. Bazı ayrıntılı açıklamalar için okuyabilirsiniz.

04_Appendice/01_More_on_infinite_trees/10_Infinite_Trees.lhs

5.1. Sonsuz Ağaçlar Hakkında

Sonsuz Yapılar kısmında bazı basit yapılar görmüştük. Ancak ağacımızdan iki özellik çıkartmıştık:

  1. tekrar eden düğümlerin olmaması
  2. düzgün sıralı ağaç olması

Bu bölümde ilk özelliği sağlamaya çalışacağız. İkincisiyle ilgili olarak da şimdilik bir şey yapmayacağız ama nasıl olabildiğince sıralı tutabileceğimizi tartışacağız.

İlk adımımız (sözde) rastgele bir sayı listesi oluşturmak olsun:

shuffle = map (\x -> (x*3123) `mod` 4331) [1..]

treeFromList fonksiyonunun tanımını hatırlayalım:

treeFromList :: (Ord a) => [a] -> BinTree a
treeFromList []    = Empty
treeFromList (x:xs) = Node x (treeFromList (filter (<x) xs))
                             (treeFromList (filter (>x) xs))

treeTakeDepth de şöyleydi:

treeTakeDepth _ Empty = Empty
treeTakeDepth 0 _     = Empty
treeTakeDepth n (Node x left right) = let
          nl = treeTakeDepth (n-1) left
          nr = treeTakeDepth (n-1) right
          in
              Node x nl nr

Programımız da şu şekilde:

main = do
      putStrLn "take 10 shuffle"
      print $ take 10 shuffle
      putStrLn "\ntreeTakeDepth 4 (treeFromList shuffle)"
      print $ treeTakeDepth 4 (treeFromList shuffle)
% runghc 02_Hard_Part/41_Infinites_Structures.lhs
take 10 shuffle
[3123,1915,707,3830,2622,1414,206,3329,2121,913]
treeTakeDepth 4 (treeFromList shuffle)

< 3123
: |--1915
: |  |--707
: |  |  |--206
: |  |  `--1414
: |  `--2622
: |     |--2121
: |     `--2828
: `--3830
:    |--3329
:    |  |--3240
:    |  `--3535
:    `--4036
:       |--3947
:       `--4242

Hey! Sonlanıyor! Ama dikkat edin, program sadece dallara koyacak veri olduğu sürece çalışacak.

Örneğin:

treeTakeDepth 4 (treeFromList [1..])

sonsuza kadar çalışacak, çünkü filter (<1) [2..] ifadesinin ilk elemanını almaya çabalayacak. Ama filter fonksiyonu sonucun boş liste olduğunu anlayacak kadar akıllı değil.

Bunlar da okur için alıştırma olarak kalsın:

  • treeTakeDepth n (treeFromList shuffle) ifadesini sonsuz döngüye sokacak bir n değeri olduğunu kanıtlayın.
  • n için bir üst sınır bulun.
  • Hiçbir shuffle listesinin programı bitiremeyeceğini kanıtlayın.

04_Appendice/01_More_on_infinite_trees/10_Infinite_Trees.lhs


04_Appendice/01_More_on_infinite_trees/11_Infinite_Trees.lhs

Bu sorunları çözmek için treeFromList ve shuffle fonksiyonlarımızı biraz değiştireceğiz.

İlk sorunumuz, shuffle fonksiyon tanımımızda sonsuz farklı sayı üreten bir yapı olmaması. Sadece 4331 farklı sayı ürettik. Bunu çözmek için daha iyi bir shuffle fonksiyonu yazalım.

shuffle = map rand [1..]
          where
              rand x = ((p x) `mod` (x+c)) - ((x+c) `div` 2)
              p x = m*x^2 + n*x + o -- bir polinom
              m = 3123
              n = 31
              o = 7641
              c = 1237

Bu shuffle fonksiyonunun (umarız ki) bir alt veya üst limiti yok. Ama daha iyi bir rastgele sayı fonksiyonu sonsuz döngüye girmeyi engellemiyor.

Genel olarak, filter (<x) xs'in boş liste olup olmadığını anlamıyoruz. Öyleyse bu sorunu çözmek için, ikili ağacımızı oluştururken hata vermeyi deneyelim. Kodumuzun bu yeni versiyonu düğümleri için şu özelliğe sahip olan bir ikili ağaç oluşturacak:

Sol alt ağaçtaki herhangi bir eleman, kökün değeriden daha küçük olmalı.

Dikkat edin ki genel olarak sıralı bir ikili ağaç olarak kalacak. Ayrıca, yapı itibarıyla, her düğüm değeri ağaçta tek olacak, tekrarlanmayacak.

treeFromList :: (Ord a, Show a) => [a] -> BinTree a
treeFromList []    = Empty
treeFromList (x:xs) = Node x left right
          where
              left = treeFromList $ safefilter (<x) xs
              right = treeFromList $ safefilter (>x) xs

Bu yeni safefilter fonksiyonu filter fonksiyonuna neredeyse denk, ancak eğer liste sonsuzsa sonsuz döngüye girmiyor. Eğer art arda 10000 değerin testi olumsuz çıkarsa, bunu aramanın sonu sayıyor.

safefilter :: (a -> Bool) -> [a] -> [a]
safefilter f l = safefilter' f l nbTry
  where
      nbTry = 10000
      safefilter' _ _ 0 = []
      safefilter' _ [] _ = []
      safefilter' f (x:xs) n =
                  if f x
                     then x : safefilter' f xs nbTry
                     else safefilter' f xs (n-1)

Şimdi programı çalıştırıp mutlu olabilirsiniz:

main = do
      putStrLn "take 10 shuffle"
      print $ take 10 shuffle
      putStrLn "\ntreeTakeDepth 8 (treeFromList shuffle)"
      print $ treeTakeDepth 8 (treeFromList $ shuffle)

Ekrana yazdırılan her değerin yazma zamanının farklı olduğunu fark etmelisiniz. Bunun sebebi Haskell'in her değeri sadece ihtiyacı olduğu zaman hesaplaması. Ve bu durumda, bu zaman ekrana yazdırılacağı zaman.

Daha da etkileyici bir şey görmek için depth değerini 8'den 100'e cikarın. RAM'inizi boğmadan çalışacak! Akış ve hafıza yönetimi Haskell tarafından doğal olarak yapılıyor.

Bu da okura alıştırma olarak kalsın:

  • deep ve nbTry için yüksek sabit değer ile bile, iyi şekilde çalışıyor gibi gözüküyor. Ama en kötü durumda üstel olabilir. treeFromList için olabilecek kötü bir liste oluşturun. ipucu: [0,-1,-1,....,-1,1,-1,...,-1,1,...] listesini düşünün.
  • safefilter fonksiyonunu şöyle tanımlamayı denedim:
  safefilter' f l = if filter f (take 10000 l) == []
                    then []
                    else filter f l

Neden çalışmadığını ve sonsuz döngüye girebileceğini açıklayın.

  • shuffle'in artan sınırlarla gerçek bir rastgele liste olduğunu farz edin. Bu yapı üzerinde biraz çalışırsanız, bu yapının 1 olasılıkla sonlu olduğunu fark edeceksiniz. Aşağıdaki kodu kullanarak (hiç safefilter yokmuş gibi doğrudan safefilter' kullanın) f için 1 olasılıkla treeFromList' shuffle'ın sonlu olduğu bir tanım bulun, ve kanıtlayın. Dikkat: bu sadece bir konjektür.

6. Teşekkürler

r/haskell ve r/programming gruplarına teşekkür ediyorum. Yorumları bana çok yardımcı oldu.

Özellikle, Emm'e, İngilizce'mi düzeltmekle uğraştığı zaman için binlerce kez teşekkür ederim.

Çevirmen Notu: Klavyemde Türkçe karakterler olmadığı için mecburen deasciifier kullanıyorum. Yazıda herhangi bir Türkçe karakter veya çeviri hatası bulursanız, beni bilgilendirirseniz, veya pull request yollayarak düzeltirseniz çok sevinirim. Daha iyi çevirilebileceğini düşündüğünüz bölümler için de bu geçerli. Teşekkürler.


Dipnotlar

[^fn-1]: Son zamanda çıkan diller onları saklamaya çalışsa da, onlar oradalar.

[^fn-2]: Hile yaptığımı biliyorum. Ama tembellikle ilgili sonra konuşacağız.

[^fn-3]: Daha cesur olanlarınız için örüntülü eşlemeyle ilgili daha kapsamlı bir açıklama şuradan okunabilir.

[^fn-4]: squareEvenSum'' fonksiyonunun diğer ikisinden daha verimli olduğuna dikkat edin. (.) fonksiyonunun sırası önemlidir.

[^fn-5]: Ki kendisi JavaScript'te JSON bulunduran bir karakter dizisi üzerinde eval çalıştırmaya çok benzer. (Çevirmen notu: JSON.parse daha iyi çözüm olabilir.)

[^fn-6]: Bu kurala bazı güvenli olmayan istisnalar da var. Ama belki hata ayıklama amacı dışında hiçbir gerçek uygulamada böyle bir kullanım görmezsiniz.

[^fn-7]: Merak edenler için: gerçek tip şöyle: data IO a = IO {unIO :: State# RealWorld -> (# State# RealWorld, a #)}. # işareti optimizasyonla ilgili, ve ben örneğimde alan yerlerini değiştirdim. Ama ana fikir bu.

[^fn-8]: Tabii ki alışana ve tamamen anlayana kadar çalışmanız gerekiyor. Ama bu yönde büyük bir adım attınız bile.