Alttaki Markdown
biçemli dokümanda birçok düzensiz deyim, yanlış biçem etiketleri, de bunun gibi sorunlar vardır çünkü bu doküman pandoc
adlı (Haskell ile yazılmış ;) bir dönüştürücü ile TeX
biçeminden Markdown
biçemine otomatik olarak çevrilmiş sonra üstünde hiçbir işlem, düzeltme yapılmamıştır.
Dokümandaki düzensizlikler bilinçli olarak düzeltilmemiştir çünkü bu arşivin amacı, Haskell The Hard Way
belgesinin özgün (true) PDF biçeminde düzenlemektir. Dolayısıyla, bu README
dosyasına bakmak ya da düzeltmek yerine dokümanın ekteki son PDF sürümünü indirmelisiniz.
This is the Turkish translation of Y. Esposito's article Learn Haskell Fast and Hard
[ Bu belge, Yann Esposito'nun Learn Haskell Fast and Hard başlıklı blog
gönderisinden (blogpost) Türkçe'ye çevirilmiştir. Yann
Espositoyannespositocom/about-me.html
yaprağında Fransa'da yaşadığını,
Amerika'daki Cisco firmasında Clojure programlama dili ile uzaktan
çalıştığını, hafta sonları ise Haskell ile Purescript ayrıca şunları
kullandığını belirtmektedir:\
[Yazılım araçları]{.underline}:
macOS üzerinde "nix with home-manager", yadm
(Eski VIM kullanıcısı ayrıca Spacemacs kullanmış biri olarak)
Emacs üzerinde doom-emacs, org-mode, org-journal, org-roam
magit, forge, github-review, weechat, wee-slack\
[Bilgi siteleri]{.underline}:
Laarc www.laarcio
Lobsters lobsters
Discover Dev www.discoverdevio
Hacker News news.ycombinatorcom
]
TL, DR1 Haskell öğrenmek için kısa, yoğun bir belge.
Tüm geliştiricilerin Haskell öğrenmesi gerektiğine inanıyorum. Herkesin süper Haskell ninjası olması gerektiğini düşünmüyorum ancak herkes Haskell'in egemen olduğu değişik yönlerini görmeli. Haskell öğrenmek belleğinizi açar.
Anaakım diller eş temelleri paylaşırlar:
-
değişken
-
döngü
-
gösterici (pointer2)
-
veri yapıları, nesneler ile klaslar (genellikle)
Ancak Haskell çok değişiktir. Bu dil daha önceden hiç duymamış olduğum bir sürü kavram kullanıyor. Bu kavramların çoğu daha iyi bir programcı olmanıza yardım edecektir.
Haskell öğrenmek zor olabilir, benim için öyleydi. Bu yazıda benim öğrenme sürecimde eksik olan nesleri size sunmaya çalışacağım.
Bu yazıyı izlemek zor olacak bunu bilerek yapıyorum; Haskell öğrenmenin kısayolu yoktur, zor olup çaba ister. Ancak bunun iyi bir nes olduğuna inanıyorum; Haskell, zor olduğu için ilginç.
Haskell öğrenmenin klasik yolu şu iki belgeyi okumaktır. İlk önce Learn You a Haskell For Greater Good3 (İyiliğiniz İçin Bir Haskell Öğrenin) sonra da Real World Haskell, O'Reilly 4 (Gerçek Yaşamda Haskell). Ben de bunun doğru yol olduğuna inanıyorum. Haskell'i doğru düzgün öğrenmek için bu belgeleri ayrıntılı biçimde okumalısınız.
Tersi biçimde, bu yazı Haskell'in ana konularının oldukça kısa, yoğun bir özeti. Kendim Haskell öğrenirken gereksinim duyup bulamadığım kimi bilgileri de ekledim.
Bu yazının beş bölümü var:
-
Giriş: Haskell'in ince yürekli olabildiğini göstermek için bir kısa örnek.
-
Temel Haskell: Haskell söz dizimi ile kimi temel kavramlar.
-
Zor Bölüm
-
Fonksiyonel stil; imperatif stilden fonksiyonel stile adım adım ilerleyen bir örnek.
-
Tipler; tipler ile 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 çalımları tricks açıklaması, IO'yu anlamak için gereksinim duyduğum gizli detay
-
Monad'ler, inanılmaz genellenebilirlikleri
-
-
Ek:
- Sonsuz ağaçlara ilişkin matematik temelinde bir tartışma.
Sonu .lhs
ile biten bir dosya adını her gördüğünüzde, dosyaya ulaşmak
için üstüne tıklayabilirsiniz (PDF doküman için geçerli değil) Dosyayı
dosyaismi.lhs
diye saklarsanız, runhaskell dosyaadi.lhs
komutuyla
çalıştırabilirsiniz.
Kimileri çalışmayabilir ancak çoğu çalışacaktır. Aşağıda linki görebilirsiniz5
(Çevirmen notu: Kodlardaki karakter dizileri Türkçeleştirilmiş, ancak değişken adları olduğu gibi bırakılmıştır. İndirilen kodlar İngilizce kaynaktan olup Türkçeleştirilmemiş olacaktır.)
-
ghc
:C
'deki gcc gibi bir derleyici. -
ghci
: Etkileşimli (interaktif) Haskell yorumlayıcısı. (REPL, Read Evaluate, Print Loop) -
runhaskell
: Bir programı derlemeden çalıştırmak için kullanılır.
Haskell üzerine pek çok belge/anlatı az bilinen bir formülü (quick sort, Fibonacci, de bunun gibi) yazmakla başlar, ben bunun direkt tersini yapacağım. İlk önce size Haskell'in süper güçlerini göstermeyeceğim. Haskell ile öteki programlama dilleri arasındaki benzerliklerle başlayacağım. Zorunlu "İyi Günler Eyge" programıyla başlayalım.
main = putStrLn "Iyi Gunler Eyge!"
Çalıştırmak için, kodu iyigun.hs
olarak saklayıp şu komutları
kullanabilirsiniz:
~ runhaskell ./iyigun.hs
Iyi Gunler Eyge!
Doğrudan kaynak kodunu da indirebilirsiniz. Aşağıdaki komutların hemen
altında linki görüp, 00_hello_world.lhs
olarak saklayıp bu komutlarla
çalıştırabilirsiniz:
~ runhaskell 00_hello_world.lhs
Hello World!
Şimdi, adınızı sorup aldığı yanıtla size "Iyi Gunler" diyen bir program yazalım:
main = do
print "Adiniz nedir?"
name <- getLine
print ("Iyi gunler, " ++ name ++ "!")
user>> ghc -o hello_you hello.hs
user>> ./hello_you Erkan
Iyi günler, Erkan!
Öncelikle, bunu birkaç imperatif dildeki benzer programlarla karşılaştıralım:
# Python (2?)
print "Adiniz nedir?"
name = raw_input()
print "Iyi Gunler %s!" % name
# Ruby
puts "Adiniz nedir?"
name = gets.chomp
puts "Iyi Gunler #{name}!"
// C
#include <stdio.h>
int main (int argc, char **argv) {
char name[666]; // <- zorlu sayi!
// Adim 665 karakterden çoksa ne olacak?
printf("Adiniz nedir?\n");
scanf("%s", name);
printf("Iyi Gunler %s!\n", name);
return 0;
}
Yapı özdeş ancak söz dizimsel değişiklikler var. Bu yazının ana kousu bu değişikliklerin nedenini açıklamak üzerine olacak.
Haskell'de bir main
fonksiyonu var olup 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 anımsamanız yeterli.
İlerlemeden önce, Haskell'in kimi temel özelliklerinin bilincine varmanız gerekiyor.
[]{#fonksiyonel label="fonksiyonel"}
Haskell fonksiyonel bir dildir. İmperatif bir dil geçmişiniz varsa, bir sürü yeni nes öğrenmeniz gerekiyor. Umarım bu yeni kavramlar size imperatif dillerde program yazarken bile yardımcı olur.
[]{#akux131llux131-statik-tip-sistemi label="akux131llux131-statik-tip-sistemi"}
Tip sistemi, C
'de, C++
'ta, Java
'da olduğu gibi sizi engellemek
yerine size yardım etmek için vardır.
[]{#saflux131k label="saflux131k"}
Genellikle fonksiyonlarınız dış ortamda bir nesi değiştirmeyecekler. Bu demek oluyor ki, bir değişkenin değerini değiştiremeyecekler, kullanıcıdan girdi alamayacaklar, ekrana yazı yazamayacaklar, yada bir füzeyi ateşleyemeyecekler. Öte yandan, paralellik sağlamak çok kolay olacak. Haskell kodunuzun nerede yan etkileri olduğunun, nerede arı olduğunun ayrımını çok açık bir biçimde yapar. Ayrıca, programınız üzerine us yürütmek de çok daha kolay olur. Çoğu yanıl, kodunuzun yalın bölümünde engellecektir. (Most bugs will be prevented in the pure parts of your program.) Daha da ötesi, Haskell'de saf fonksiyonlar temel bir kural izlerler: Bir fonksiyona özdeş parametreler vermek hep özdeş değerler döndürür.
[]{#üsengeclik label="üsengeclik"}
Üşengeçlik, genelde alışılmadık bir dil tasarım seçimidir. Haskell'de varsayılan işlem olarak her nes yalnızca gereksinim olduğunca işlenir. Bunun sonuçlarından biri, sonsuz yapıları işlemek için çok kusursuz bir yol sunmasıdır.
Son uyarı da Haskell kodunu hangi yöntemle okumanız gerektiği üzerine.
Benim için, bilimsel anlatıları okumak gibi. Kimi bölümleri çok açık
ancak bir formül gördünüzde odaklanarak yavaşça okuyun. Ayrıca, Haskell
öğrenirken, sıradışı söz dizimsel (syntactical) detayları anlamamanız
gerçekten önemli değil. Ancak >>=
, <$>
, <-
de bunun gibi
herhangi bir değişik sembol görürseniz, görmezden gelip kodun akışını
izleyin.
Şu biçimde fonksiyon tanımlamaya alışmış olabilirsiniz:
C:
int f(int x, int y) {
return x*x + y*y;
}
JavaScript:
function f(x,y) {
return x*x + y*y;
}
Python:
def f(x,y):
return x*x + y*y
Ruby:
def f(x,y)
x*x + y*y
end
Scheme:
(define (f x y)
(+ (* x x) (* y y)))
Son olarak, Haskell yolu da budur:
f x y = x*x + y*y
Yalın. Parantez yok, def
yok.
Unutmayın, Haskell fonksiyonlar ile tipleri sıkça kullanır. Bu yüzden, onları tanımlamak oldukça kolaydır. Söz dizimi, gereklilik nedeniyle özellikle öyle düşünülmüştür.
Zorunlu olmasa da, fonksiyonlar için tip bilgisi genellikle ayrıca girilir. Zorunlu değildir, çünkü derleyici sizin için çıkarım yapacak kadar soybağdır. Yine de tipleri yazmak iyi bir düşüncedir, çünkü kodun anlaşılmasına yardımcı olur.
Bakalım ne yolla oluyormuş:
-- Tipleri belirtmek icin :: imini kullaniyoruz
f :: Int -> Int -> Int
f x y = x*x + y*y
main = print (f 2 3)
~ runhaskell 20_very_basic.lhs
13
Şimdi bunu deneyelim:
f :: Int -> Int -> Int
f x y = x*x + y*y
main = print (f 2.3 4.2)
Şu yanılı 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 ki 4.2
bir tüm sayı (Int) değil. Çözümü ise: f
fonksiyonu
için şimdilik bir tip belirtmeyerek, tip çıkarımını Haskell'e bırakmak.
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 de bunun gibi 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, 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
Ne? Bu değişik tip de neyin nesi?
Num a => a -> a -> a
İlk önce, sağdaki a -> a -> a
bölgesine bakalım. Anlamak için adım
adım ilerleyerek ş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 özdeş a tipine olan fonksiyon tipi a -> a -> a herhangi bir a tipinden iki argümanın özdeş a tipine olan fonksiyon tipi
a -> a -> a
'da a
imine tip değişkeni diyoruz. (type variable). Bu
f
'nin iki argümanı olduğu, girilen argümanlar ile fonksiyon sonucunun
özdeş tipten olduğu anlamına geliyor. Tip değişkeni a
, başka bir sürü
değer alabilir. Örneğin Int
, Integer
, Float
...
Dolayısıyla C
'deki gibi zorunlu olarak bir fonksiyon için int
,
long
, float
, double
de bunun gibi tip belirtmek yerine, herhangi
bir dinamik tip tabanlı dilde olduğu gibi yalnızca bir fonksiyon
tanımlıyoruz.
Buna kimileyin parametrik çokbiçimlilik (parametric polymorphism) de deniyor. Yaşamda her istediğinizin olması gibi bir nes.
Genellikle a
herhangi bir tip olabilir, örneğin String
yada Int
,
ancak Trees
gibi daha karışık tipler, başka fonksiyonlar da olabilir.
Ancak buradaki tipimiz Num a =>
ile başlıyor.
Num
bir tip klası. (type class). Tip klasları tip grupları gibi
düşünülebilir. Num
klası yalnızca sayı gibi davranan tipleri içerir.
Daha doğrusu, Num
, belli bir fonksiyon listesinin, özellikle (+)
ve
(*)
fonksiyonlarının, etki ettiği klastır.
Tip sınıfları güçlü bir dil yapısıdır. Onlarla inanılmaz güçlü nesler yapabiliriz. Buna daha sonra yine değineceğiz.
Sonuç olarak, Num a => a -> a -> a
şu demek oluyor:
Diyelim ki a
, Num
tip klasına ait bir tip. Bu da a
tipinden
a -> a
tipine bir fonksiyon.
Evet, beklenenden değişik. Esasında Haskell'de hiçbir fonksiyonun gerçekten iki argümanı yoktur. Onun yerine, her fonksiyonun yalnızca bir argümanı vardır. Ancak anımsamalı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ğunu gözlemleyin:
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 adsız fonksiyonlar yaratmamıza olanak sağlar. Bunlara anonim fonksiyonlar diyoruz. Özdeş nesi lambda notasyonu (gösterimi) ile şöyle de yazabilirdik:
g = \y -> 3*3 + y*y
Burada \ kullanılıyor, çünkü λ (lambda) imine benzemekte olup ayrıca ASCII dizisinin içindedir.
Fonksiyonel programlamaya alışık değilseniz beyniniz yanmaya başlamış olmalı. Artık gerçek bir uygulama yazma anı.
Ancak 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 rasyonel (Fractional) sayılar
için, hem de Integer
(tüm sayı tipi) için geçerli bir gösterim. 2.4
rasyonel bir sayı olduğu için 3
de rasyonel bir sayı olarak
yorumlanıyor.
Fonksiyonumuzu birbirinden değişik tiplerle çalışmaya zorlarsak, yanıl 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 yanıl veriyor. İki parametre de özdeş tipten olmak zorunda.
Bunun kötü bir düşünce olduğunu, derleyicinin sizin için bir tipten ötekine dönüşümü yapması gerektiğini düşünüyorsanız, WAT6 adlı korkunç ayrıca komik videoyu kesinlikle izlemelisiniz:
Bu bölüme yalnızca göz gezdirmenizi öneriyorum. Her an yararlanacağınız bir kaynak gibi düşünün. Haskell'in birçok özelliği vardır. Burada da pek çoğu eksik. Notasyon alışılmadık gelirse yine buraya dönün.
İki deyimin denk olduğunu belirtmek için
x^n herhangi bir n integral tipi icin (Int yada 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! Yine de önce Data.Ratio
modülünü
içeri aktarmanız gerekiyor:
$ ghci
....
Prelude> :m Data.Ratio
Data.Ratio> (11 \% 15) * (5 % 3)
11 % 9
[] ⇔ 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"] ⇔ YANIL! String ≠ Integral
[1..4] ⇔ [1,2,3,4]
[1,3..10] ⇔ [1,3,5,7,9]
[2,3,5,7,11..100] ⇔ YANIL! O denli soybağ degilim!
[10,9..1] ⇔ [10,9,8,7,6,5,4,3,2,1]
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"
ÖNEMLİ: Gerçek kodda, yazıyı simgelemek üzere Char
listesi
kullanmamalısınız. Genel olarak Data.Text
kullanmalısınız. ASCII
karakter akımını (stream) simgelemek istiyorsanız, onun için de
Data.ByteString
kullanmalısınız.
Bir ikili demetin tipi (a,b)
'dir. Demetlerdeki elemanlar birbirinden
değişik 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) ⇒ YANIL: fst :: (a,b) -> a
snd (x,y,z) ⇒ YANIL: snd :: (a,b) -> b
Parantezlerin kimisinden kurtulmak için ($)
ile (.)
fonksiyonlarını
kullanabilirsiniz.
-- Esasinda:
f g h x ⇔ (((f g) h) x)
-- $ imi kendisinden deyimin sonuna
-- dek 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))
Anımsatma:
x :: Int ⇔ x Int tipinde herhangi bir deger alabilir
x :: a ⇔ x herhangi bir tip olabilir
x :: Num a => a ⇔ x Num tip klasi kapsamında olan
herhangi bir a tipi olabilir
f :: a -> b ⇔ f a'dan b'ye bir fonksiyondur
f :: a -> b -> c ⇔ f a'dan (b→c)'ye bir fonksiyondur
f :: (a -> b) -> c ⇔ f (a→b)'den c'ye bir fonksiyondur
Bir fonksiyonu tanımlamadan önce tipini belirtmenin zorunlu olmadığını anımsayın. Haskell genelde tip çıkarımını sizin için kendisi yapar. Ancak genelde tipleri belirtmek iyi bir pratik olarak görülür.
[]{#orta-notasyon label="orta-notasyon"}
square :: Num a => a -> a
square x = x^2
^
iminin iç notasyon (infix notation) kullandığını özellikle
gözlemleyin. Her iç notasyona bağlı bir ön notasyon (prefix notation)
vardır. O durumda, onu parantez içine koymalısınız.
square' x = (^) x 2
square'' x = (^2) x
Soldaki ile sağdaki x
'leri silebiliriz. Buna
square''' = (^2)
Değişken adlarında '
kullanabildiğimize özel ilgi gösterin
square
⇔ square'
⇔ square''
⇔ square'''
Salt değer (absolute value) fonksiyonu yazalım:
absolute :: (Ord a, Num a) => a -> a
absolute x = ıf x >= 0 then x else -x
Özellikle ilgi gösterin ki Haskell'deki if .. then .. else
notasyonu,
C'deki ¤?¤:¤
operatörüne bayağı benzer. else
bölümünü unutmanız
olası değil.
Başka bir denk versiyonu:
absolute' x
| x >= 0 = x
| otherwise = -x
Haskell'de paragraf başı ile boşluklar önemlidir. Python'daki gibi, kötü boşluklar kodunuzu bozabilir.
[Zor bölüm şimdi başlıyor.][]{#zor-kux131sux131m label="zor-kux131sux131m"}
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 kodu evrimleştireceğiz. Son durum hem çok daha elegant hem de kolay anlaşılabilir olacak.
Aşağıdaki problemi çözelim:
Verilen bir tüm sayı listesindeki düz (ikiyle kalansız bölünebilen)
sayıların toplamını alın. Örnek: [1,2,3,4,5] ⇒ 2 + 4 ⇒ 6
Fonksiyonel ile imperatif yaklaşımların arasındaki ayrımı göstermek üzere, 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, öteki dillerden ayrık olarak, değişkenler yada for
döngüleri yoktur. Döngüler olmaksızın özdeş sonucu elde etmenin bir yolu
özyinel (recursion)'dir
Önemli: Özyinel, imperatif dillerde genellikle yavaş olarak algılanır. Fonksiyonel programlamada genellikle durum bu değildir. Çoğu kez Haskell özyinel fonksiyonları verimli biçimde işler.
İşte özyinel fonksiyonun C versiyonu. Kolaylık için tüm sayı listesinin
ilk 0
değeri ile bittiğini varsaydığımı özellikle gözlemleyin.
int evenSum(int *list) {
return accumSum(0,list) ;
}
int accumSum(int n, int *list) {
int x;
int *xs;
if (*list == 0) { // liste bos ise
return n;
}
else {
x = list[0]; // x listenin ilk elemani olsun
xs = list+1; // xs listenin ilk elemani dışında geri kalani olsun
if ( 0 == (x\%2) ) { // x duz ise
return accumSum(n+x, xs);
}
else {
return accumSum(n, xs); // Burada sorun var ?
}
}
}
Bu kodu belleğinizde tutun. Şimdi onu Haskell'e çevireceğiz. Ancak ilk önce, size burada kullanacağımız üç yalın ancak kullanışlı fonksiyonu tanıtmam gerekiyor:
even :: Integral a => a -> Bool
head :: [a] -> a
tail :: [a] -> [a]
even
bir sayının düz (ikiyle kalansız bölünebilen) 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ı dışında tüm elemanlarını döndürür.
tail :: [a] -> [a]
tail [1,2,3] ⇒ [2,3]
tail [3] ⇒ []
tail [] ⇒ HATA
Görebileceğiniz gibi, herhangi bir boş olmayan l
listesi için,
l ⇔ (head l):(tail l)
İlk Haskell çözümümüz. evenSum
fonksiyonu bir listedeki tüm düz
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: 7
*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 nes doğru gözüküyor olmalı. Esasında buradaki pek çok nes geliştirilebilir. Öncelikle, tipi genelleyebiliriz.
evenSum :: Integral a => [a] -> a
Daha sonra, where
yada let
kullanarak alt fonksiyonlar
tanımlayabiliriz. Bu biçimde accumSum
fonksiyonu modülümüzün üst düzey
ad 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
Sonra, örüntü 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ü eşleme nedir? Genel parametre adları yerine değerlerin kendisini kullanın. 8
foo l = if l == [] then <x> else <y>
demek yerine, yalnızca şöyle
diyorsunuz:
foo [] = <x>
foo l = <y>
Ancak örüntü eşleme bundan daha ötesidir. Ek olarak karışık bir değerin iç verisini (inner data) yoklamanın bir yoludur Ş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. Ek olarak kodumuzu daha kısa de okunaklı kılıyor.
Haskell'de
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
Her şeyi daha da iyi yapmak için, üst derece fonksiyonları kullanmalıyız. Peki bu canavarlar nelerdir? Üst derece fonksiyonlar, başka fonksiyonları parametre olarak alan fonksiyonlardır.
Kimi ö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 fonksiyon ile [a]
tipinde
bir listeyi argüman olarak alır. Bu listeden yalnızca bu fonksiyon
çalıştırıldığında True
dönen elemanları döndürür.
Sonraki adımımız, döngüye benzer bir işlemi başarmak. foldl
fonksiyonunu listede adım adım ilerlerken yanda bir değer biriktirmek
için kullanacağız. foldl
fonksiyonu esasında şu master yapıyı alıp:
myfunc list = foo initialValue list
foo accumulated [] = accumulated
foo tmpValue (x:xs) = foo (bar tmpValue x) xs
Şu duruma çevirir:
myfunc list = foldl bar initialValue list
Bu gizemli nesnenin gerçekte ne yolla ç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
Ancak Haskell üşengeç olduğu için (f z x)
'in değerini bulmadan
yalnızca yığının üstüne koyar. Bu yüzden genelde foldl
yerine foldl'
kullanırız; foldl'
, foldl
fonksiyonunun üşengeç olmayan
versiyonudur. Üşengeç ile naüşengeç kavramlarını anlamıyorsanız,
tasalanmayın, kodu foldl
ile foldl'
eşit neslermiş gibi izleyin.
Şimdi evenSum
fonksiyonumuzun yeni durumu şö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 yalınlaştırabiliriz.
Böylece mysum
adında geçici bir fonksiyon yaratmak zorunda kalmayız.
-- Versiyon 7
-- Genelde yalnızca gerek duydugunuz fonksiyonlari
-- iceri almak daha iyi bir yontemdir
import Data.List (foldl')
evenSum l = foldl' (\x y -> x+y) 0 (filter even l)
Sonra doğal olarak, özen gösterelim ki:
(\x y -> x+y) ⇔ (+)
Son olarak,
-- Versiyon 8
import Data.List (foldl')
evenSum :: Integral a => [a] -> a
evenSum l = foldl' (+) 0 (filter even l)
foldl'
çok kolay bir fonksiyon sayılmaz. Alışık değilseniz, üzerinde
biraz çalışmalısınız.
Burada ne olup bittiğini görebilmek için adım adım ilerleyelim:
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) anlatır.
(f . g . h) x ⇔ f ( g (h x))
Bu operatörden fonksiyonumuzda
-- Versiyon 9
import Data.List (foldl')
evenSum :: Integral a => [a] -> a
evenSum = (foldl' (+) 0) . (filter even)
Ayrıca, kimi bölümleri daha iyi açıklamak için yeniden adlandırabiliriz:
-- 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 deyimlerle 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 ayrım kısalık. Ancak esasta, ayrım 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 düz 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)
Yalnızca bir tane daha transformasyon fonksiyonu ekledik, hepsi bu. 9
map (^2) [1,2,3,4] ⇔ [1,4,9,16]
map
fonksiyonu açıkça bir listenin tüm elemanlarını etkiler.
Fonksiyon tanımının içinde herhangi bir nes değiştirmek zorunda
kalmadık. Ancak ek olarak, fonksiyonunuz üzerine daha matematiksel
olarak us yürütebiliyorsunuz. Ayrıca fonksiyonunuzu ötekileriyle
değişmeli kullanabiliyorsunuz. Dolayısıyla, 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
Genellemenin sonuna geldiğimizi düşünüyorsanız, oldukça yanılıyorsunuz. Örneğin, bunu yalnızca liste değil başka herhangi bir özyinelemeli türde kullanmanın yolları var. Ne yolla olduğunu bilmek istiyorsanız, size şu eğlenceli anlatıyı okumanızı öneririm: Muz, Mercek, Zarf, Dikenli Tellerle Fonksiyonel Programlama - Meijer, Fokkinga, Paterson 10
Bu örnek size arı fonksiyonel programlamanın ne denli güzel olduğunu göstermeli. Ne yazık ki, arı fonksiyonel programlama her kullanıma tam uygun değil. Ya da en azından öyle bir programlama dili şu ana dek yok.
Haskell'in büyük güçlerinden biri de alana özel dil (domain specific language) yaratma yeteneğidir, böylece programlama paradigmasını (yaklaşımını) değiştirebilirsiniz.
Esası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, ne yolla, hangi anda ö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ün e değinmeliyiz: Tipler.
TL, DR (Çok uzundu okumadım):
-
type Ad = BaskaTip
yalnızca bir takma ad olup derleyiciAd
ileBaskaTip
arasında bir ayrım gözetmez. -
data Ad = AdYapısı BaskaTip
yapısında ayrım vardır. -
data
anahtar sözcüğü özyinelemeli yapılar yaratabilir. -
deriving
gizemlidir, sizin için fonksiyonlar yaratır.
Haskell'de tipler hem güçlü hem statiktir.
Peki bu neden önemli? Çünkü bu, yanıllardan kaçınmanıza yüksek derecede yardımcı olur. Haskell'de yanılların çoğu derleme aşamasında yakalanır. Bunun asıl nedeni de tip çıkarımının derleme sırasında yapılmasıdır. Örneğin tip çıkarımı hangi yanlış parametreyi hangi yerde kullandığınızı yakalar.
Statik tip sistemi hızlı çalıştırma için genelde önemlidir. Ancak çoğu statik tip sistemli diller kavramları genellemede kötüdür. Haskell'in kurtarıcı yanı, tipleri kendi kendine çıkarım yaparak bulabilmesidir.
Yalın 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 niceliği ile 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 (pre-processor) kullanarak üst-programlama yöntemlerine 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. Ancak daha karmaşık fonksiyonlar için söz dizimini izlemek biraz daha zor olabilir: örnek için what does haskell have to do with c11 bakabilirsiniz.
C++'ta bir fonksiyonun değişik tiplerle çalışacağını 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 duygusunu yaratır. Ancak dinamik tip sistemli dillerden ayrı olarak çoğu yanıl, çalışma anından önce yakalanır. Genelde Haskell kodu > derleniyorsa, salt amaçladığınız işi yapıyordur.
Kendi tiplerinizi oluşturabilirsiniz. İlk önce takma adlarla (aliases), dolayısıyla tip eşanlamlılarıyla (synonyms) başlayalım.
You can construct your own types. First, you can use aliases or type synonyms.
type Name = String
type Color = String
showInfos :: Name -> Color -> String
showInfos name color = "Ad: " ++ name
++ ", Renk: " ++ color
name :: Name
name = "Erkan"
color :: Color
color = "Mavi"
main = putStrLn $ showInfos name color
Ancak bu yeterli koruma sağlamıyor. showInfos
fonksiyonuna verdiğiniz
parametrelerin yerini değiştirip çalıştırmayı deneyin:
putStrLn $ showInfos color name
Derlenecek , çalışacak. Esasında, Name
, Color
ile String
tiplerini
birbiriyle değiştirebilirsiniz, bir ayrım yaratmayacak. Derleyici
hepsine eşitmiş gibi davranacak.
Başka bir yöntem de data
anahtar sözcüğünü kullanarak kendi
tiplerinizi yaratmak.
data Name = NameConstr String --NameConstr : ad yapici
data Color = ColorConstr String --ColorConstr : renk yapici
showInfos :: Name -> Color -> String
showInfos (NameConstr name) (ColorConstr color) =
"Name: " ++ name ++ ", Color: " ++ color
name = NameConstr "Erkan"
color = ColorConstr "Mavi"
main = putStrLn $ showInfos name color
Ancak şimdi showInfos
için parametrelerin yerlerini değiştirirseniz,
derleyici yanıl verecek! Dolayısıyla bu bir daha yapmayacağınız olası
bir hata olup kaçınmak için tek yapmanız gereken biraz daha uzun yazmak.
Yapıcıların da birer fonksiyon olduğuna özellikle bakın:
NameConstr :: String -> Name
ColorConstr :: String -> Color
data
anahtar sözcüğünün genel söz dizimi de şöyledir:
data TipAdi = YapiciAdi [tipler]
| YapiciAdi2 [tipler]
| ...
Genel kullanım tip adı ile yapıcı adının özdeş olması yönündedir.
Örnek:
data Complex = Num a => Complex a a
Kütük (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
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)
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.
Bu veri tipini ekrana yazdırmak (Show
), karakter dizisinden çevirmek
(Read
), eşitliğini test edip (Eq
) 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üretme
demektir.) Yakında kendi show
fonksiyonunuzu ne biçimde
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)
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 denli 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 ancak zor anlaşılır bir sembolik notasyonu.
Ağacımız için daha iyi bir gösterim kodu, öylesine yazalım. Ben genel olarak ağaçları daha iyi göstermek için bir fonksiyon yazarken eğlendim, bu bölümü izlemeyi zor buluyorsanız atlayabilirsiniz, bir sorun olmaz.
Değiştirmemiz gereken kimi nesler var. BinTree
tipımizden
deriving (Show)
bölümünü kaldırıyoruz. Ayrıca, BinTree tipimizi (Eq
ve Ord
)'un klaslarından türetmek, eşitlik de 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)
bölümü olmadan Haskell sizin için bir show
metodu
oluşturma. Biz show
metodu için kendi versiyonumuzu yazacağız. Bunu
başarmak için, yeni oluşturduğumuz BinTree a
'nin Show
tip klası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 ancak kaygılanmayın. Daha sıradışı nesneleri de göstermesi için birkaç iyileştirme yaptım.
-- BinTree'a nin Show tip klasına 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 kod sırasına 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 ile 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 öteki 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 hiç değişmeden kalıyor.
Şimdi, görelim ne yolla oluyormuş:
main = do
putStrLn "Tum sayi ikili agaci:"
print $ treeFromList [7,2,4,8,1,3,6,21,12,23]
Tum 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 kod
sırasında gösteriliyor. İzleyen her kod :
imi 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ği ile 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 kod sırasını bu yüzden :
ile başlatmıştım. (kök dışında)
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 (equivalent,"eşdeğer"):
print ( treeFromList (
map treeFromList
[ map treeFromList ["YO","DAWG"]
, map treeFromList ["I","HEARD"]
, map treeFromList ["I","HEARD"]
, map treeFromList ["YOU","LIKE","TREES"] ]))
ş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'
Yinelenen ağaçların eklenmediğine özen gösterin; "I","HEARD"
'e denk
gelen yalnızca bir ağaç var. Bunun için (neredeyse) hiçbir nes yapmadık
çünkü Tree yapısını Eq
tip klasından türettik.
Bu yapının ne denli iyi olduğunu görebiliyor musunuz: Yalnızca 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!
Haskell'in üşengeç olduğu sıkça söylenir. Esasında biraz titizseniz, Haskell'in non-strict12 ( hemen işlem yapmayan, kesin olmayan) olduğunu söylemelisiniz. Üşengeçlik yalnızca non-strict dillerin ortak bir uygulamasıdır. Öyleyse "non-strict" net biçimde ne anlama geliyor? Haskell vikisiden alıntılayalım:
Yalınlaştırma ( kalkülasyon için matematiksel terim) dıştan içe doğru
ilerler. dolayısıyla (a+(b*c))
'yi ele alıyorsanız, önce +
'yi
yalınlaştırır, sonra iç (b*c)
'yi yalınlaştırırsınız.
Ö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 tümünü kalküle etmek yerine, yalnızca gereksinimi
olan elemanları, gereksinimi olduğunda kalküllü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...]
ayrıca çoğu fonksiyon da onlarla çalışır. Ek olarak, Haskell'de, bizim
take'
fonksiyonumuza denk bir take
fonksiyonu vardır.
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, bütün 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 dek 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ışıp ş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, ancak 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 üzerine burada daha öte konuşmayacağım. map
'in öteki veri
yapılarına genellemesiyle ilgileniyorsanız, functorları ile 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
Buraya geldiyseniz kutlamalar! Şimdi gerçekten çok zor bölüm başlayabilir.
Benim gibiyseniz, fonksiyonel stili anlamış olmalısınız. Ayrıca üşengeçliğin bir norm olmasının avantajlarını da biraz anlamış olmalısınız. Ancak gerçek bir program yapmaya nereden başlamanız gerektiğini bilmiyorsunuz. Özellikle de şu soruların yanıtlarını:
-
Yan etkilerle nasıl baş edilir?
-
Neden IO (girdi-çıktı) ile baş etmek için imperatifliğe benzer bir notasyon var?
Karmaşık yanıtlara tetikte olun. Ancak hepsi sonunda çok yararlı.
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 sıra kodun 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. İnce bir ayrım olarak, burada yalın fonksiyonları doğrudan (directly) kullanamazsınız. Yalın fonksiyonları kullanmak için örneğinaction2 (saffonksiyon x)
yazabilirsiniz.
Bu bölümde size IO kullanmayı anlatacağım, ancak hangi yolla çalıştığını değil. Haskell'in arı ile arı olmayan kodları hangi yolla ayırt ettiğini göreceksiniz.
Söz dizimindeki detayları anlamaya çalışmak için duraksamayın. (Don't stop because you're trying to understand the details of the syntax) Yanıtlar 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ç biçimde , do
bloğunun içindeki her deyimin IO a
tipinde olduğuna ilgi gösterelim.
main = do
putStrLn "Enter ... " :: IO ()
getLine :: IO String
print Something :: IO ()
Ayrıca <-
iminin etkisine de ilgi gösterelim.
do
x <- something -- bir nesler
something :: IO a
tipinde ise x :: a
'dir.
IO
kullanımıyla ilgili başka bir önemli ayrıntı da şudur: do
bloğunun içindeki tüm kod sıraları şu iki türden birindeki olmalı:
action1 :: IO a
-- bu durumda, a = ()
yada
deger <- action2 -- ki burada
-- bar z t :: IO b
-- deger :: b
Bu iki tür komut aksiyonları sıralamanın iki değişik yolunu anlatıyor. Bu tümcenin anlamı önümüzdeki bölümün sonunda netleşecek.
Şimdi programımızın nasıl davrandığına bakalım. Örneğin, kullanıcı sıradışı bir nes 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! Değişik bir yanıl mesajından sonra programımız çöktü! İlk iyileştirmemiz daha anlaşılır bir yanıl mesajı vermek olsun.
Bunu yapmak için önce bir neslerin yanlış gittiğini saptayabilmemiz
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 tane parametre alan bir tiptir. Tanımı da
şudur:
data Maybe a = Nothing | Just a
Bu bir değer okumaya yada oluşturmaya çalışırken bir yanıl olduğunu
belirtmenin güzel bir yoludur. maybeRead
fonksiyonu bunun iyi bir
örneği. Bu read
'e 13 benzer bir fonksiyon, ancak bir nesler yanlış
giderse dönen değer Nothing
olacak. Bir değer okuyabilirse, dönen
değer Just <değer>
olacak. Bu fonksiyonu çok anlamaya çalışmayın;
read
'den daha alt düzey 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 biçimde ilerleyen bir fonksiyon
tanımlayalım: Karakter dizisinin formatı (biçemi) yanlışsa, Nothing
döndürülecek. Tersi durumda, ö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 (denetlemek)
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."
Kodun yanıl vermesi durumunda, iyi sayılabilecek bir yanıl mesajı göstermiş olalım.
main
fonksiyonunun do
bloğundaki her deyimin tipinin IO a
formatında olduğuna gözatın. Tek bilmediğiniz yapı error
. Ancak
error msg
de gerekli tipi alıyor. (burada IO ()
).
Bu programda bilincine varmanız gereken nes, tanımladığımız
fonksiyonların tipleri. Yazdığımız fonksiyonlar arasında yalnızca bir
tanesinin tipinde IO
var: main
. Bu demek oluyor ki main
arı
olmayan bir fonksiyon. Ancak main
içinde arı bir fonksiyon olan
getListFromString
kullanılıyor. Dolayısıyla yalnızca bakarak bile
fonksiyonların arı olup olmadıklarını anlayabilirsiniz.
Peki arılık neden önemlidir? Bir sürü avantajından üçü şunlar:
-
Arı kod üzerine us yürütmek arı olmayan kod üzerine us yürütmekten çok daha kolaydır.
-
Arılık sizi yan etkilerden ötürü kolayca test edemeyeceğiniz yanıllardan korur.
-
Arı fonksiyonları herhangi bir sırayla yada eş anlı olarak hiçbir risk olmadan bilgisaydırabilirsiniz.
İşte bu yüzden, olabildiğince yüksek sayıda kodu arı fonksiyonlarla yazmalısınız.
Sonraki adımımız kullanıcı geçerli bir yanıt girene dek yinelecek biçimde sormak olsun.
İlk bölümü olduğu gibi 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 dek yine 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. Kimileri
bunu şöyle açıklıyor:
"IO içinde [Integer] var"
Bunun arkasındaki (ardındaki detayları anlamak istiyorsanız, sonraki bölümü okumanız gerekecek. Ancak IO'yu yalnızca kullanmak istiyorsanız, konuların üzerinden gidip tipler üzerine düşünmeniz gerektiğini anımsayın.
Son olarak, main
fonksiyonumuz çok daha kolay:
main :: IO ()
main = do
list <- askUser
print $ sum list
IO'ya girişimizi bitirdik. Biraz hızlıydı, değil mi? Anımsamanız gereken temel nesler şunlar:
-
do
bloğunun içinde, her deyimIO a
tipinde olmalı. Bu sizi belli deyimlere sınırlıyor. Örneğin,getLine
,print
,putStrLn
, de bunun gibi. -
Arı fonksiyonları olabildiğince arı olmayan bölümlerin dışında tutmaya çalışın, işin olabildiğince büyük bölümünü arı fonksiyonlara yaptırın.
-
IO a
,a
tipinde bir eleman döndüren IO aksiyonu demektir.IO
aksiyonu simgeler,IO a
esasında bir fonksiyonun tipidir. Daha ötesi ile ilgileniyorsanı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.
Arı olan ile arı olmayan kodları ayırmak için, main
tüm ortamın
durumunu değiştiren bir fonksiyon olarak tanımlanır.
main :: World -> World
Bir fonksiyon, yalnızca bir tek bu tipi içeriyorsa 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
ile w3
)
bind
yada (>>=)
fonksiyonu yaratıyoruz. bind
ile artık geçici
adlara gereksinimimiz yok.
main =
action1 >>= action2 >>= action3 >>= action4
Bonus: Haskell'in şöyle bir söz dizimsel (sentaks) kolaylığı var:
main = do
v1 <- action1
v2 <- action2 v1
v3 <- action3 v2
action4 v3
Neden böyle alışılmadık bir söz dizimi kullandık, ayrıca bu IO
tipi
net olarak nedir? Anlaşılmaz duruyor ancak, değil.
Şimdilik, programımızın arı bölümlerini bir kenara bırakıp arı olmayan bölümler üzerine 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 arı olmayan kodu imperatif
gösterecek ölçüde güçlüdür. Örneğin, isterseniz Haskell'de while
yapısı yaratabilirsiniz. , Üstelik ayrıca IO
ile uğraşman için
imperatif stil genellikle daha uygundur.
Ancak notasyonun alışılmışın dışında olduğu gözünüze ilişmiş olmalı. Şimdi bunun nedenlerini detaylıca tartışalım.
Arı olmayan bir dilde, ortamın durumu devasa, gizli bir global değişken olarak görülebilir. Bu gizli değişkene dildeki tüm fonksiyonlar aracılığıyla erişilebilir. Örneğin herhangi bir fonksiyon içinde bir dosya ile okuma/yazma işlemleri yapabilirsiniz. Dosyanın var olup olmaması "ortam"ınızın alabileceği olası durumlardır.
Haskell'de bu durum gizli değildir. Direkt tersine, Haskell'de main
'in
ortamınızın durumunu değiştirme olasılığı olduğu ayrıca özel olarak
belirtilir. Bu fonksiyonun tipi şunun gibi bir nesdir:
main :: World -> World
Bu değişkene tüm fonksiyonların erişimi yoktur. Erişimi olan fonksiyonlar arı değildir. (Global) ortam değişkenine erişimi olmayan fonksiyonlar ise arıdır. 14
Haskell, ortamın durumunu main
fonksiyonuna bir girdi değişkeni olarak
görür. Ancak main
'in esas tipi şuna daha yakındır: 15
main :: World -> ((),World)
()
tipi birim tipidir. Burda görülecek bir nes yok.
Şimdi bunu belleğimizde 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 anımsayalım:
World -> (a,World)
Burada a
sonucun tipi oluyor. Örneğin getChar
fonksiyonu bu durumda
World -> (Char,World)
tipindedir.
Özen gösterilmesi gereken öteki ayrıntı ise kalkülasyon / değerlendirme
sırası. Örneğin f a b
'yi kalküle ederken birden çok seçeneğiniz var:
-
önce
a
'yi, sonrab
'yi, sonra daf a b
'yi kalküle et. -
önce
b
'yi, sonraa
'yi, sonra daf a b
'yi kalküle et. -
a
'yi veb
'yi paralel olarak, sonra daf a b
'yi kalküle et.
Bu böyle çünkü dilin arı yanında çalışıyoruz.
Şimdi, main
fonksiyonuna bakarsanız, ilk kod sırası ikinci kod
sırasından önce kalküle etmemiz gerektiği açık, çünkü ikinci kod
sırasında birincidekinden elde ettiğiniz bir değeri parametre olarak
kullanıyorsunuz.
Bu işe yarıyor. Derleyici her adımda yeni bir gerçek global ortam tanımı
/ kodu (id) sağlıyor. Gizli olarak, print
şöyle işliyor:
-
ekrana bir nes yazdır
-
global ortamın id'sini değiştir.
-
((), yeni global ortam id'si)
değerini döndür.
Şimdi, main
fonksiyonunun stiline bakarsanız, biraz değişik olduğunu
göreceksiniz. Tıpatıp özdeşini 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, ancak biraz daha değişik. Tüm şu geçici w
'lere bakın.
Çıkarılacak öğreti şu: Arı fonksiyonel dillerdeki naif IO uygulamaları sıradışıdır!
Şanslıyız ki, bu sorunu çözümlemek için daha iyi bir yol var. Bir desen görüyoruz, her kod sırası şu biçimde:
let (y,w') = action x w in
Herhangi bir kod sırası için ilk x
argümanı gerekmese bile. Dönen
sonucun tipi bir ikili, (sonuç, yeniGlobalOrtamDegeri)
Her f
fonksiyonu şuna benzer bir tipi içermek zorunda:
f :: World -> (a,World)
Her durumda özdeş deseni izlediğimize özel ilgi gösterin:
let (y,w1) = action1 w0 in
let (z,w2) = action2 w1 in
let (t,w3) = action3 w2 in
...
Her aksiyon hiçten(0) n'ye dek parametre alabilir. Ayrıca özellikle, her aksiyon bir üstündeki kod sırasını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
...
Buradan doğal olarak actionN w :: (World) -> (a,World)
.
Önemli: Göz atılması gereken iki desen var:
let (x,w1) = action1 w0 in
let (y,w2) = action2 x w1 in
ile
let (_,w1) = action1 w0 in
let (y,w2) = action2 w1 in
Şimdi biraz oyunculuk yapalım. Geçici global ortam sembolünü "yok
edelim". İki kod sırasını 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))
Ancak anımsayın ki (World -> (a,World))
IO aksiyonlarının tipidir.
Daha açık olması için ona yeni bir ad verelim:
type IO a = World -> (a, World)
Kimi fonksiyon örnekleri:
getLine :: IO String
print :: Show a => a -> IO ()
getLine
global ortamı parametre olarak alıp (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,
ancak esasında iki argüman almaktadır. İlki ekrana yazdırılacak
değerdir, ikincisi de global ortamın durumudur. Sonrasında ise
((),World)
ikilisini döndürür. Bu fonksiyonun global ortamın durumunu
değiştirdiği ancak başka bir veri üretmediği anlamına gelir.
Bu tip, bind
fonksiyonun tipini yalınlaştırmamıza yardımcı olur:
bind :: IO a
-> (a -> IO b)
-> IO b
Bu demek oluyor ki bind
fonksiyonu iki IO aksiyonunu parametre olarak
alıp başka bir IO aksiyonunu geri döndürüyor.
Önemli desenleri yeniden anımsayı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) Bundaki oyunu görmediyseniz, bu kez üç sıra kodla
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 nes ilginizi çekti mi? Evet, artık hiçbir yerde geçici World değişkenleri kullanılmıyor. Büyücülü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 kodlarda 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 yalınlaştırmadım. Doğal olarak daha
yalın 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 arı değerleri IO bağlamına sokmak için kullanılan genel bir yoldur.
putInIO
için kullanılan genel ad return
'dür. Haskell öğrenirken bu
oldukça kötü bir adlandırmadır çünkü Haskell'deki return
alışık
olduğunuzden çok değişiktir.
Ö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 duruma 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.
(>>)
ile (>>=)
olmadan ne yolla olacağını düşünün.
Artık bu gizemi açıklayabiliriz: IO
bir monaddir. Monad olmak, do
notasyonu ile belirli söz dizimsel kolaylıklara iye olmak demektir.
Ancak temel olarak, kodunuzun akışını kolaylaştıracak belirli kod
desenlerine erişim sağlar.
Önemli ayrıntılar:
-
Monadlar etkilerle ilgili olmak zorunda değildir! Arı monadlar da vardır.
-
Monadlar daha çok sıralama ile ilgilidir.
Haskell'de Monad
bir tip klasıdır. Bu klasın bir üyesi olmak için
(>>=)
ile return
fonksiyonlarını sağlamalısınız. (>>)
fonksiyonu
(>>=)
fonksiyonundan türer. Temel olarak Monad
tip klası şö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
-- Yalnızca gecmisteki (tarihsel) nedenlerden dolayi oldugunu dusundugum
-- bu fonksiyonu degerlendirmeye (dikkate) almayabilirsiniz
fail :: String -> m a
fail = error
Notlar:
-
class
anahtar sözcüğü 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 adlandırma olurdu, çünkü o tip grubu anlamına geliyor. Bir tipin bir klasa bağlı olması için, bir klası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ğinIO a
, ancak eş anlı olarakMaybe a
,[a]
, de bunun gibi. -
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 ~
Monad
tip klasının üyesi olan bir sürü değişik tip vardır. Tanımlaması
en kolay olanlarından biri de Maybe
'dir. Maybe
değerlerinden oluşan
bir diziniz varsa, etkilemek için monadları kullanabilirsiniz. Uzayıp
giden if then else
yapılarından kurtulmak için oldukça kullanışlı
bir yoldur.
Karmaşık bir banka işlemi düşünün. Kontoda kalan paranız hiçin altına düşmeden belli işlemleri yapabilecek yeterlikte paranız varsa, 700€ kazanma olanağınız/payınız olsun.
deposit value account = account + value --para yatirma
withdraw value account = account - value --para cekme
--para kazanmaya pay 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
Şimdi Maybe
kullanıp monadlardan yararlanarak 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
Kötü değil, ancak 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 şirinleştirmenin iyi bir yolu olduğunu
gösterdik. Özellikle bakın ki, bu tip bir kod düzenlemesi, özellikle
Maybe
, pek çok imperatif dilde de uygulanabilir. Esasında, bu tip bir
yapıyı normalde de yapıyoruz.
Önemli bir ayrıntı: Dizideki ilk elemanın Nothing
olarak kalküle
edilmesi tüm işlemi durduracaktır. Bu demek oluyor ki, tüm kod
sıralarını işletmiyorsunuz. Üşengeçlik sayesinde bunun için ekstradan
bir nes 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 temel örnek ile Maybe
monadının ne denli kullanışlı olabileceğini
de ne yolla kullanılabileceğini gördük. Ancak daha iyi bir örnek için
listelere bakalım.
Liste monadı deterministik (belirlenimci) olmayan kalkülasyonları 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
Açıkça oyunbazlı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. Arı dillerdeki belli kavramlar monad kullanımı ile yalınlaştırılmıştır. Monadlar özellikle şunlar için kullanışlıdır:
-
IO
-
deterministik olmayan kalkülasyonlar
-
(sözde) gelişigüzel sayı üretimi
-
konfigürasyon durumunu tutma
-
yazma durumu
Buraya dek beni izleyebildiyseniz, başardınız demektir! Monadları öğrendiniz. 16
Bu bölüm doğrudan Haskell'le ilgili değil. Kimi ayrıntılı açıklamalar için okuyabilirsiniz.
Sonsuz Yapılar bölümünde kimi yalın yapılar görmüştük. Ancak ağacımızdan iki özellik çıkartmıştık:
-
yinelenen (duplike) düğümlerin (node) olmaması
-
düzgün sıralı ağaç olması
Bu bölümde ilk özelliği sağlamaya (iletmeye) çalışacağız. İkincisiyle ilgili olarak şimdilik pek bir nes yapmayacağız ancak onu ne yaparak olabildiğince düzgün sıralı tutabileceğimizi tartışacağız.
(In this section we will try to keep the first property. Concerning the second one, we must relax it but we'll discuss how to keep it as much as possible.)
İlk adımımız (sözde) gelişigüzel bir sayı listesi oluşturmak olsun:
shuffle = map (\x -> (x*3123) `mod` 4331) [1..]
treeFromList
fonksiyonunun tanımını anımsayalı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 biçimde:
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! Ancak bir göz atın, program yalnızca dallara koyulabilecek veri olduğu sürece çalışacak.
Örneğin:
treeTakeDepth 4 (treeFromList [1..])
sonsuza dek çalışacak çünkü filter (<1) [2..]
deyiminin ilk elemanını
almaya çabalayacak. Ancak filter
fonksiyonu sonucun boş liste olduğunu
anlayacak ölçüde soybağ değil.
Bunlar da okur için alıştırma olarak kalsın:
-
treeTakeDepth n (treeFromList shuffle)
deyimini sonsuz döngüye sokacak birn
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.
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 ayrık sayı üreten
bir yapı olmaması. Yalnızca 4331
ayrık 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 yada üst limiti yok.
Ancak daha iyi bir gelişigüzel 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 yanıl
verdirmeyi deneyelim. Kodumuzun bu yeni versiyonu düğümleri için şu
özelliği olan bir ikili ağaç oluşturacak:
Sol alt ağaçtaki herhangi bir eleman, kökün değeriden daha küçük olmalı.
Özellikle göz atın ki genel olarak sıralı bir ikili ağaç olarak kalacak. Ayrıca, yapı olarak, her düğüm değeri ağaçta tek olacak, yinelenmeyecek.
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 liste sonsuzsa, sonsuz döngüye girmiyor. Ardarda 10000 değerin
testi olumsuz çıkarsa, bunu aramanın sonu olarak değerlendiriyor.
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 anının değişik olduğunu sezinlemelisiniz. Bunun nedeni Haskell'in her değeri yalnızca gereksinimi olduğu anda kalküle etmesi. Ek olarak bu durumda, bu an ekrana yazdırılacağında.
Daha da etkileyici bir nes görmek için depth
değerini 8
'den 100
'e
çıkarın. RAM'inizi boğmadan çalışacak! Akış ile bellek yönetimi Haskell
aracılığı ile doğal olarak yapılıyor.
Bu da okura alıştırma olarak kalsın:
-
deep
venbTry
için yüksek değişmez (sabit) değer ile bile, iyi biçimde çalışıyor gibi gözüküyor. Ancak 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ışmayıp sonsuz döngüye girebileceğini açıklayın. *
shuffle
'in artan sınırlarla gerçek bir gelişigüzel liste olduğunu
varsayın. Bu yapı üzerinde biraz çalışırsanız, bu yapının bir olasılıkla
sonlu olduğunu yakalayacaksınız. Aşağıdaki kodu kullanarak (hiç
safefilter
yokmuş gibi doğrudan safefilter'
kullanın) f
için bir
olasılıkla treeFromList' shuffle'ın sonlu olduğu bir tanım bulup
kanıtlayın. Önemli: Bu yalnızca bir konjektür.
r/haskell17 ile r/programming18 esenlikler diliyorum. Yorumları bana çok yardımcı oldu.
Özellikle, Emm19'e, İngilizcemi düzeltmekle uğraştığı süre için binlerce kez mutsözlerimi iletirim.
Çevirmen Notu (Joomy Korkut): Klavyemde Türkçe karakterler olmadığı için zoraken deasciifier20 kullanıyorum. Yazıda herhangi bir Türkçe karakter yada çeviri yanlışı bulursanız, beni bilgilendirirseniz, yada pull request yollayarak düzeltirseniz çok sevinirim. Bu, daha iyi çevirilebileceğini düşündüğünüz bölümler için de geçerli. Esenlikler.
[]{#dipnotlar label="dipnotlar"}
No.1
Özgür Belge Lisansı
Footnotes
-
Too Long, Didn't Read Çok uzundu, okumadım ↩
-
Son dönemde çıkan diller onları saklamaya çalışsa da, onlar oradalar. ↩
-
learnyouahaskellcom
, No Starch Press ↩ -
www.realworldhaskellorg
, O'Reilly ↩ -
yannespositocom/Scratch/en/blog/Haskell-the-Hard-Way/code/01_basic/10_Introduction/00_hello_world.lhs
↩ -
aldatma yaptığımı biliyorum. Ancak üşengeçlik konusuna sonra yine değineceğiz. ↩
-
Daha yiğit olanlarınız için örüntü eşleme üzerine daha kapsamlı bir açıklama şuradan okunabilir. ↩
-
squareEvenSum''
fonksiyonunun öteki ikisinden daha verimli olduğuna özellikle ilgi gösterin(.)
fonksiyonunun sırası önemlidir. ↩ -
eprints.eemcs.utwentenl/7281/01/db-utwente-40501F46.pdf ↩
-
bartoszmilewskicom/2009/10/21/what-does-haskell-have-to-do-with-c ↩
-
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.) ↩ -
Bu kurala uymayan kimi güvenli olmayan dış durumlar da var. Ancak belki yanıl ayıklama amacı dışında hiçbir gerçek uygulamada böyle bir kullanım görmezsiniz. ↩
-
İlgilenenler için: gerçek tip şöyle:
data IO a = IO {unIO :: State# RealWorld -> (# State# RealWorld, a #)}
.#
imi optimizasyonla ilgili olup ben örneğimde alan yerlerini değiştirdim. Ancak ana düşünce bu. ↩ -
Doğal olarak alışıp tümüyle anlayana dek çalışmanız gerekiyor. Ancak bu yönde büyük bir adım attınız bile. ↩
-
reddit.com/r/haskell ↩
-
reddit.com/r/programming ↩
-
githubcom/Emm, Danimarkab, www.imperocom ↩
-
githubcom/joom/turkish-deasciifier.vim ↩