Daha önce Haskell'in statik bir tip sistemi olduğundan bahsetmiştik. Haskell programlarında yer alan her bir ifadenin tipi derleme esnasında derleyici tarafından bulunabilir, bu da yazdığımız kodun daha güvenli olmasını sağlar. Eğer bir boolean tipli bir değeri bir sayıya bölmeye çalışırsak kodumuz derlenmez. Bu istenen bir özelliktir çünkü programlarımızda bir hata olduğunu program çalışırken çöktüğünde anlamaktansa derleme esnasında anlamak daha iyidir. Haskell'de her şeyin bir tipi vardır ve bu sayede derleyici programı derlemeden önce program hakkında bolca düşünebilir.
Java ve Pascal'ın aksine Haskell'de tip çıkarımı vardır, yani derleyici ifadelerin tiplerini tahmin edebilir. Bir sayı yazdığımızda, Haskell'e yazdığımız şeyin bir sayı olduğunu belirtmemize gerek yoktur. Benzer şekilde yazdığımız diğer ifadelerin ya da fonksiyonların da tipini tek tek belirtmemiz gerekmez. Haskell'e giriş yaparken tiplerden çok yüzeysel bir şekilde bahsetmiştik. Ama tip sistemini kavramak Haskell öğrenmenin çok önemli bir kısmını oluşturuyor.
Tipleri bütün ifadelerin sahip olduğu etiketler gibi düşünebiliriz. Tipler bize ifademizin hangi kategorilere dahil edilebileceğini gösterir. Örneğin True bir boolean değerdir, "merhaba" ise bir string'tir.
Şimdi GHCI'dan faydalanarak bazı ifadelerin tiplerine bakacağız. Bunu yapmak içinse :t komutunu kullanacağız. :t komutu ardına yazılan geçerli herhangi bir ifadenin bize tipini gösterecektir. Hadi deneyelim.
:t komutunu bir ifade ile kullandığımızda sonuç "ifadenin kendisi :: ifadenin tipi" şeklinde olacaktır. :: "...'nin tipi" şeklinde okunabilir. Belirgin tiplerin gösteriminde her zaman büyük harfle başlanır. 'a' karakterinin tipi Char'dır. Bu da karakter kelimesinin (character) kısaltması oluyor. True'nun tipi Bool'dur. Ama o da ne? "MERHABA!" string'inin tipine baktığımızda aldığımız cevap [Char] oldu. Köşeli parantezleri listeleri göstermek için kullanıyoruz. Yani tipimiz Char listesi oldu. Listelerin aksine farklı uzunluklardaki tuple'lar veya çokuzlular ayrı tiplere sahip oluyor. (True, 'a') ifadesinin tipi (Bool, Char) iken ('a', 'b', 'c')'nin tipi (Char, Char, Char) şeklindedir. 4 == 5 ifadesinin değeri her zaman False olacağı için ifadenin tipi de Bool'dur.
Fonksiyonların da kendilerine ait tipleri vardır. Bir fonksiyonu tanımlarken eğer istersek fonksiyonun tipini açık bir şekilde belirtebiliriz. Fonksiyonların tipini belirtmek yazdığımız fonksiyon çok kısa olmadığı durumlarda tipi yazmamaya kıyasla daha iyi bir yöntem olarak kabul edilir. Bundan sonra yazdığımız her fonksiyonun tipini de belirteceğiz. Daha önceden bir string'i filtreleyerek sadece büyük harflerin kaldığı liste tanımlamamızı hatırlıyor musun? Tipini belirttiğimizde şu şekilde gözüküyor.
removeNonUpperCase :: [Char] -> [Char]
removeNonUpperCase st = [ c | c <- st, c `elem` ['A'..'Z']]
removeNonUpperCase fonksiyonunun tipi [Char] -> [Char]. Bu da fonksiyonun Char Kaynakça yani stringleri başka stringlere eşlediği yani parametre olarak bir string aldığı ve yine bir string döndüğü anlamına geliyor. [Char] tipi String ile aynı anlama gelir, bu yüzden de tipimizi removeNonUpperCase :: String -> String şeklinde de yazabilirdik. Bu fonksiyonun tipini belirtmek zorunda değildik çünkü derleyici zaten bu fonksiyonu incelerken fonksiyonun stringlerden stringlere tanımlı olduğu çıkarımını yapabilecekti. Peki birden fazla parametreye bağlı fonksiyonların tipini nasıl yazarız? Hemen üç tane tam sayı alan ve bunları toplayan basit bir fonksiyon yazalım.
addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z
Yazdığımız fonksiyonda ilk üç tip parametrelerin tipiyken sonuncusu sonucun tipidir. Parametreleri birbirinden ayırmak için -> işaretini kullanırız ama bunu yaparken sonucun tipini parametrelerin tiplerinden ayırmak için herhangi bir şey yapmayız. İlerleyen bölümlerde neden fonksiyon tiplerimizi parametre ve sonucu ayırarak, örneğin Int, Int, Int -> Int veya benzer başka bir şekilde değil de bütün tipler arasına -> koyarak yazmamız gerektiğini açıklayacağız.
Eğer yazdığınız fonksiyonların tipini belirtmek istiyorsanız ama fonksiyonun tipinin ne olması gerektiğinden emin değilseniz fonksiyonunuzu tipini belirtmeden tanımlayabilir ve tipine sonrasında GHCI'dan :t komutu ile bakabilirsiniz. Fonksiyonlar sonuçta birer ifadedir ve bu yüzden :t komutu fonksiyonlar üzerinde de çalışır.
Şimdi bazı yaygın tiplere bakalım.
Int, integer yani tam sayının kısaltması. 7 bir Int olabilir ama 7.2 bir Int olamaz. Int belli sınırlar içinde değerler alabilir, yani minimum ve maksimum değerleri vardır. 32 bit bilgisayarlarda Int'in en büyük değeri 2147483647, en küçük değeri de -2147483648'dir.
Integer da Int gibi tam sayıları göstermek için kullanılıyor. Aralarındaki fark şu: Int'in aksine Integer'ın alabileceği değerler belirli sınırlar arasında yer almak zorunda değil. Bu yüzden Integer'ı çok çok büyük sayıları göstermek için kullanabiliyoruz (gerçekten çok büyük). Diğer yandan, Int ile yaptığımız işlemler daha verimlidir.
factorial :: Integer -> Integer
factorial n = product [1..n]
ghci> factorial 50
30414093201713378043612608166064768844377641568960512000000000000
Float tek duyarlıklı kayan nokta sayıların tipidir.
circumference :: Float -> Float
circumference r = 2 * pi * r
ghci> circumference
25.132742
Double ise çift duyarlıklı kayan nokta sayıları göstermek için kullanılır.
circumference' :: Double -> Double
circumference' r = 2 * pi * r
ghci> circumference' 4.0
25.132741228718345
Bool boolean veya boole'sal değerlerin tipidir. Mümkün Bool değerleri iki tanedir: True ve False.
Char karakterleri temsil eder. Tek tırnaklar arasına yazılır. Karakter listesine string denir.
Tuple'lar veya çokuzluların tipi uzunluklarına ve içerdikleri değerlerin tipine bağlıdır. Bu yüzden teorik olarak sonsuz sayıda farklı tuple tipi vardır ve bunların her birine bu kitapta değinemeyiz. Bunlar arasından boş tuple olan () aynı zamanda bir tiptir ve tip olan () mümkün olan tek bir değer barındırır: ()
head fonksiyonun tipi sizce ne olabilir? Bu fonksiyon bize herhangi bir listenin ilk elemanını veriyor. Öyleyse tipi ne olabilir? Bakalım!
:t head
head :: [a] -> a
Peki buradaki a nedir? Bir tipse neyin tipidir? Hatırlarsanız tip isimleri büyük harfle başlıyordu, öyleyse a bir tip olamaz. Öyleyse a nedir sorusunu cevaplayalım: a bir tip değişkenidir, yani a herhangi bir tip olabilir. Bu biraz diğer dillerdeki generics özelliğine benziyor, ama tip değişkenleri Haskell'de belli bir tipin özelliklerinden faydalanmayan genel fonksiyonlar yazmamızı oldukça kolaylaştırdığı için genericsten çok daha güçlüdür diyebiliriz. Tipinde tip değişkenleri olan fonksiyonlara polymorphic fonksiyon diyoruz. head fonksiyonunun tipi bize bu fonksiyonun herhangi bir tipte elemanları olan bir liste kabul ettiği ve sonuç olarak bu tipe sahip bir eleman verdiğini gösteriyor.
Tip değişkenleri bir karakterden daha uzun isimlere sahip olabilir, ancak yine de genellikle a, b, c, d gibi tek harfli isimler tercih ediyoruz.
fst fonksiyonunu hatırlıyor musunuz? Bu fonksiyon herhangi bir sıralı ikilinin ilk elemanını almaya yarıyordu. Hemen tipine bakalım.
:t st
fst :: (a, b) -> a
fst fonksiyonunun içinde iki tip olan bir tuple alıp tupleın ilk elemanı ile aynı tipte bir eleman döndüğünü görüyoruz. a ve b birbirinden bağımsız olduğu için fonksiyonu uyguladığımız sıralı ikilinin elemanlarının tiplerinin aynı olmak zorunda olmadığı gibi farklı olmak zorunda da olmadığını belirtmiş oluyoruz.
Bir tipsınıfı, bazı davranışları tanımlayan bir çeşit arayüzdür. Eğer bir tip, bir tipsınıfının parçası ise; bunun anlamı bu tipin, tipsınıfının tanımladığı davranışları desteklediğidir. Nesne tabanlı programlamadan gelen bir çok insanın kafası, nesne tabanlı dillerdeki sınıflar ile tipsınıflarının benzer olduğunu düşündükleri için karışmaktadır. Aslında, değiller. Bunları bir çeşit Java arayüzleri(interfaces) gibi düşünebilirsiniz, sadece çok daha iyiler.
==
fonksiyonunun tür imzası nedir?
ghci> :t (==)
(==) :: (Eq a) => a -> a -> Bool
Note: eşittir operatörü de bir fonksiyondur. Yani +
,*
,-
,/
operatörlü de ve neredeyse tüm operatörler de. Eğer bir fonksiyon sadece özel karakterler içeriyorsa, bu öntanımlı olarak içerlek (infix) fonksiyon sayılır. Eğer bir operatörün türünü kontrol etmek istersek, başka bir fonksiyona geçirmek istersek veya normal fonksiyon olarak kullanmak istersek, parantezlerle çevreleyebiliriz.
İlginç. Yeni bir şey görüyoruz; =>
işareti. =>
işaretinden önceki her şey sınıf kısıtı (class constraint) olarak bilinir. Önceki tür tanımını şöyle okuyabiliriz; eşitlik fonksiyonu aynı türde iki değer alır ve Bool
türünde bir değer döner. Bu iki değişkenin türü Eq
sınıfının üyesi olmalıdır. Eq
sınıf kısıtıydı.
Eq
tip sınıfı eşitliği sınamak için bir arayüz sağlar. İki değeri arasındaki eşitliğin sınanmasının anlamlı olduğu herhangi bir tür Eq
sınıfının bir üyesi olmalıdır. IO hariç tüm standart Haskell türleri ve fonksiyonları Eq
tip sınıfının bir parçasıdır.
elem
fonksiyonun türü (Eq a) => a -> [a] -> Bool
olarak ayarlıdır çünkü bir listede bizim istediğimiz üyenin bulunup bulunmadığını anlamak için üyeleri karşılaştırırken ==
kullanır.
Bazı temel tip sınıfları:
Eq
eşitlik sınamayı destekleyen tipler için kullanılır. Üyeleri ==
ve /=
fonksiyonlarını kullanır. Yani, eğer Eq
sınıf sabitine sahip bir tip değişkeni barındıran bir fonksiyon varsa, içerisinde bir yerlerde ==
veya /=
fonksiyonlarını kullanır. Bir önceki hariç tuttuklarımız hariç tüm türler Eq
tip sınıfının üyesidir, yani eşitlikleri sınanabilir.
ghci> 5 == 5
True
ghci> 5 /= 5
False
ghci> 'a' == 'a'
True
ghci> "Ho Ho" == "Ho Ho"
True
ghci> 3.432 == 3.432
True
Ord
sıralanabilir türler içindir.
ghci> :t (>)
(>) :: (Ord a) => a -> a -> Bool
Fonksiyonlar hariç gördüğümüz neredeyse tüm türler Ord
'un bir parçasıdır. Ord
tip sınıfı >
,<
,>=
ve <=
gibi tüm standart karşılaştırma fonksiyonlarını kapsar. compare
fonksiyonu Ord
üyesi iki aynı tür alır ve sırasını döner. Ordering
ise GT
,LT
, veya EQ
değerlerini alabilen bir türdür. Değerlerin anlamı ise greater than(büyüktür), lesser than(küçüktür) ve equal(eşittir) demektir.
Ord
tip sınıfının üyesi olmak için, öncelikle prejisli ve ayrıcalıklı Eq
klübüne üye olmak gerekir.
ghci> "Abrakadabra" < "Zebra"
True
ghci> "Abrakadabra" `compare` "Zebra"
LT
ghci> 5 >= 2
True
ghci> 5 `compare` 3
GT
Show
tip sınıfının üyeleri ise metin(string) olarak gösterilebilirlerdir. Fonksiyonlar hariç gördüğümüz tüm türler Show
tip sınıfının üyesidir. Show
tipsınıfının en çok kullanılan fonksiyonu show
'dur. Show
tip sınıfının üyesi bir değer alır ve metin olarak gösterir.
ghci> show 3
"3"
ghci> show 5.334
"5.334"
ghci> show True
"True"
Read
tip sınıfı bir nevi Show
tip sınıfının tersi gibidir. read
fonksiyonu bir metin(string) alır ve Read
tip sınıfına üye olan tipe çevirir.
ghci> read "True" || False
True
ghci> read "8.2" + 3.8
12.0
ghci> read "5" - 2
3
ghci> read "[1,2,3,4]" ++ [3]
[1,2,3,4,3]
Şimdiye kadar çok iyi. Tekrar, tüm gördüğümüz türler, bu tipsınıflarına dahildir. Peki ama, eğer sadece read "4"
yazarsak ne olur?
ghci> read "4"
<interactive>:1:0:
Ambiguous type variable `a' in the constraint:
`Read a' arising from a use of `read' at <interactive>:1:0-7
Probable fix: add a type signature that fixes these type variable(s)
GHCI'ın bize söylediği şey, burada bizim hangi türde değer istediğimizi bilmiyor olduğudur. Önceki kullanımlarımıza dikkat ederseniz read
fonksiyonundan sonra sonucu doğurabileceği bir şeyler yaptık. Bu şekilde, GHCI read
fonksiyonunu kullandığımızda hangi türde geri dönüş beklediğimize dair bir çıkarımda bulunabildi. Mantıksal(boolean) bir değer ile beraber kullandığımızda Bool
türünde bir veri beklediğimiz sonucunu çıkarttı. Fakat şimdi, bizim Read
tip sınıfına ait bir veri tipinde dönüş istediğimizi bilse de bunun hangi veri tipi olduğunu bilemiyor. read
fonksiyonun tür imzasına bakarsak;
ghci> :t read
read :: (Read a) => String -> a
Gördünüz mü? Read
tip sınıfında bir değer döndürüyor, ancak biz bu değer başka bir türü belli işlemde kullanmazsak, hangi türe dönüştüreceğini bilemeyecek. Bu yüzden aleni tür açıklamalarını(type annotation) kullanabiliyoruz. Tür açıklamaları, ifadenin türününün ne olduğunun açıkca belirtilebilmesi için kullanılıyor. İfadenin sonuna ::
ekleyerek kullanbiliriz. Bakınız;
ghci> read "5" :: Int
5
ghci> read "5" :: Float
5.0
ghci> (read "5" :: Float) * 4
20.0
ghci> read "[1,2,3,4]" :: [Int]
[1,2,3,4]
ghci> read "(3, 'a')" :: (Int, Char)
(3, 'a')
Çoğu ifadenin türünü derleyici kendisi çıkarımla bilebilir. Fakat bazen, derleyici read "5"
için Int
mi yoksa Float
mı döndürmesi gerektiğine karar veremez. Türü anlayabilmek için Haskell'in read "5"
ifadesini çalıştırması gerekir. Fakat Haskell statik türlü bir dil olduğundan dolayı, derlenmeden (veya GHCI üzerinde çalışmadan) önce türü bilmesi gerekir. Sonuç olarak Haskell'e şunu söylemek zorundayız; "Hey, şu anda bilmiyor olsan da bu ifade bu türe sahip!".
Enum
üyeleri ardışık sıralı olabilen tip sınıfıdır. Enum
tip sınıfının temel faydası aralık listelerinde kullanılabilir türler olmasıdır. Ayrıca ardıl ve ölcülleri bulunabilir ki succ
ve pred
fonksiyonları bu iş içindir. Bu sınıftaki standart türler ()
,Bool
,Char
,Ordering
,Int
,Integer
,Float
ve Double
'dır.
ghci> ['a'..'e']
"abcde"
ghci> [LT .. GT]
[LT,EQ,GT]
ghci> [3 .. 5]
[3,4,5]
ghci> succ 'B'
'C'
Bounded
üyeler bir alt ve üst limite sahiptirler.
ghci> minBound :: Int
-2147483648
ghci> maxBound :: Char
'\1114111'
ghci> maxBound :: Bool
True
ghci> minBound :: Bool
False
minBound
ve maxBound
ilginçtirler çünkü tür imzası şöyledir: (Bounded a) => a
. Bir bakıma polimorfik sabitlerdir.
Tüm ikililer(tuple) eğer elemanları öyleyse Bounded
sınıfına aittirler.
maxBound :: (Bool, Int, Char)
(True,2147483647,'\1114111')
Num
numarasal bir tip sınıfıdır. Üyeleri sayı gibi davranma özelliği olan şeylerdir. Bir sayı tipini sınayalım;
ghci> :t 20
20 :: (Num t) => t
Görünen o ki tüm sayılar bir de polimorfik sabitlerdir. Num
tip sınıfına ait herhangi bir tür gibi davranabilirler.
ghci> 20 :: Int
20
ghci> 20 :: Integer
20
ghci> 20 :: Float
20.0
ghci> 20 :: Double
20.0
Bunlar Num
tip sınıfına ait türlerdir. Eğer *
fonksiyonun türünü sorarsak sayı kabul ettiğini göreceğiz.
ghci> :t (*)
(*) :: (Num a) => a -> a -> a
Aynı türde iki sayı alır ve yine aynı türde bir sayı döner. Bu yüzden (5 :: Int) * (6 :: Integer)
tip hatası verecek ancak 5 * (6 :: Integer)
beklendiği gibi çalışacak ve Integer
bir sonuç üretecektir çünkü 5
sayısı Int
veya Integer
gibi davranabilir.
Num
tip sınıfına katılabilmek için bir tür Show
ve Eq
tip sınıflarıyla da arkadaş olmalıdır.
Integral
de bir sayısal tip sınıfıdır. Num
tip sınıfı tüm sayıları kapsar, integral sayılar ve gerçek sayılar dahil. Integral
tip sınıfı ise sadece integral sayıları kapsar. Int
ve Integer
bu tip sınıfına dahildir.
Floating
sadece kayan noktalı sayıları kapsar, yani Float
ve Double
fromIntegral
sayılarla uğraşmak için kullanışlı bir fonksiyondur. fromIntegral :: (Num b, Integral a) => a -> b
şeklinde bir tip tanımı vardır. Tip imzasından görebileceğimiz gibi integral bir sayı alır ve daha genel bir sayısal değer döndürür. Bu, integral ve kayan noktalı sayılarla bir arada çalışmak gerektiğinde kullanışlıdır. Örneğin, length
fonksiyonunun tip tanımı (Num b) => length :: [a] -> b
olmak yerine, length :: [a] -> Int
olarak belirlenmiştir. Sanırım bunun ardında tarihsel veya başka bir sebep var, fakat bana sorarsanız aptalca. Her neyse, bir listenin boyutunu alıp buna 3.2
eklemek istersek, Int
türüne kayan noktalı sayı eklemeye çalıştığımız için hata alacağız. Bunun etrafından dolaşmak için şöyle yapabiliyoruz; fromIntegral (length [1,2,3,4]) + 3.2
.
Dikkat edin, fromIntegral
birden çok sınıf sabitine sahip. Bu tamamen doğru ve görebileceğiniz gibi parantezler içinde virgülle ayrılmış.