2.6. TIETOKONE LASKIMENA 23 Edellä esitetty Ydin-Haskell on hyvin lähellä sitä kieltä, jota GHCi (Glasgow Haskell Compiler, Interactive) sekä muut Haskell-järjestelmät suostuvat ymmärtämään. Esimerkiksi: _ / _ \ /\ /\/ (_) / /_\// /_/ / / GHC Interactive, version 6.0.1, for Haskell 98. / /_\\/ / / http://www.haskell.org/ghc/ \ /\/ /_/\ / _ Type :? for help. Loading package base... linking... done. Prelude> (let { f = \ x -> case x of _ x == 0 -> Just 1 x > 0 -> case f (x - 1) of { Just x -> Just (x*x ) ; Nothing -> Nothing } x < 0 -> Nothing } in f) 5 Just 120 Prelude> (let { f = \ x -> case x of _ x == 0 -> Just 1 x > 0 -> case f (x - 1) of { Just x -> Just (x*x ) ; Nothing -> Nothing } x < 0 -> Nothing } in f) (-3) Nothing Laajennetaan vielä Ydin-Haskellia ymmärtämään vakiomäärittelyt. Määrittely on muotoa vakionnimi :: Tyyppilauseke vakionnimi = lauseke Tyypinmäärittelyrivi ei ole pakollinen, mutta se on suositeltava. Funktiovakioita voidaan määritellä myös siten, että vakionnimen ja yhtäsuuruusmerkin väliin kirjoitetaan yksi tai useampi muuttuja: vakionnimi :: Tyyppilauseke vakionnimi x y z = lauseke Tämä tarkoittaa (oleellisesti) samaa kuin vakionnimi :: Tyyppilauseke vakionnimi = λx λy λz lauseke Määrittelyt (sekä vakioiden että tyyppien) sijoitetaan tiedostoon, jonka nimi alkaa isolla alkukirjaimella ja päättyy päätteeseen.hs ja jonka ensimmäinen rivi kuuluu Module Nimi where, missä Nimi on tiedoston nimi ilman.hs-päätettä. Tiedostossa jokainen määrittely alkaa rivin vasemmasta laidasta (jatkorivit noudattavat aiemmin esitettyä sisennyskäytäntöä). Esimerkiksi voidaan tehdä seuraavanlainen Fact.hs: module Fact where fact :: Integer -> Maybe Integer
24 LUKU 2. OHJELMOINTI LASKENTANA fact n = case n of _ n < 0 -> Nothing n == 0 -> Just 1 n > 0 -> case fact (n - 1) of Just m -> Just (n * m) Nothing -> Nothing jonka jälkeen voidaankin käynnistää GHCi komennolla ghci Fact: _ / _ \ /\ /\/ (_) / /_\// /_/ / / GHC Interactive, version 6.0.1, for Haskell 98. / /_\\/ / / http://www.haskell.org/ghc/ \ /\/ /_/\ / _ Type :? for help. Loading package base... linking... done. Compiling Fact ( Fact.hs, interpreted ) Ok, modules loaded: Fact. *Fact> fact 5 Just 120 *Fact> 2.7 Kohti aitoa Haskellia Ydin-Haskell on yksinkertainen ja sikäli kiva kieli käsin laskettavaksi. Koneen toimiessa laskimena on kuitenkin paljon tärkeämpää, että käytetty kieli on ohjelmien kirjoittajalle käytännöllinen kuin että se olisi laskijalle yksinkertainen. Sen takia, vaikka pääosin Ydin-Haskell onkin sellaisenaan Haskellia, on itse Haskell-kieli paljon rikkaampi. Seuraavassa esitellään muutama tärkeä laajennus, jotka eivät enää kuulu Ydin-Haskelliin mutta joita käytännössä käytetään jatkuvasti. 2.7.1 Tyyppialiakset Mille tahansa tyypille voidaan antaa nimi eli alias. Tämä tapahtuu type-määrittelyllä. Esimerkiksi String on tyypin [Char] alias: type String = [Char] Tyyppialiasta voidaan käyttää kaikissa tilanteissa, joissa kyseistä tyyppiä voidaan käyttää. Tyyppialias voi olla parametrisoitu siinä kuin mikä tahansa muukin tyyppikoostin. Tyyppialias ei ole uusi tyyppi. Sen tärkein rajoite on se, että type-määrittelyt eivät voi olla keskenään rekursiivisia (kuin korkeintaan algebrallisen tyyppimäärittelyn kautta kulkemalla). Esimerkki 12 Seuraava määrittely ei ole sallittu: type
2.7. KOHTI AITOA HASKELLIA 25 2.7.2 Monikot Tyyppi data Pair α β = Pair α β on niin yleinen funktio-ohjelmoinnissa, että sille on Haskellissa oma syntaksinsa: parityyppi kirjoitetaan tyyppilausekkeissa (α, β), sen koostin on pilkkuoperaattor (, ), jota voidaan käyttää sekä tyyliin (, ) 2 3 että (2, 3). Kumpikin muodoista (, ) x y ja (x, y) kelpaavat hahmoiksi case-lausekkeissa. Itse asiassa edellä sanottu pätee kaikille monikoille, ei pelkästään pareille: esimerkiksi kolmikkotyyppi on (α, β, γ), ja yksi sen lausekkeista on (3, 4, 5). Esimerkki 13 Seuraavassa on muutama monikkoarvo tyyppeineen: 1. (3, 4) :: (Integer, Integer) 2. ((0, a ), (λx x), ()) :: ((Integer, Char), α α, ()) 3. (,, ) b 46 (λx (x, x)) :: (Char, Integer, α (α, α)) 2.7.3 Funktiosidonta Funktiosidonta on yleistetty versio edellä esitetystä vakionmäärittelystä. Yleisesti funktiosidonta on seuraavaa muotoa: f p 1,1... p 1,k g 1,1 = e 1,1...... g 1,m1 = e 1,m1 f p n,1... p n,k g n,1 = e n,1... g n,mn = e n,mn Tämä tarkoittaa oleellisesti samaa kuin seuraava Ydin-Haskellin määritelmä: f = λx 1 λx k case (x 1,..., x n ) of { (p 1,1,..., p 1,k ) g 1,1 e 1,1 g 1,m1 e 1,m1 ; ; (p n,1,..., p n,k ) g n,1 e n,1 g n,mn e n,mn }, missä x i ovat muuttujia, jotka eivät esiinny koko funktiosidonnassa. Esimerkki 14 Moduli Fact.hs voidaan kirjoittaa näinkin: module Fact where fact :: Integer -> Maybe Integer fact n n < 0 = Nothing
26 LUKU 2. OHJELMOINTI LASKENTANA n == 0 = Just 1 n > 0 = case fact (n - 1) of Just m -> Just (n * m) Nothing -> Nothing Esimerkki 15 Seuraavalla funktiolla (joka löytyy Haskellin varuskirjastosta Prelude) voidaan curry ttu funktio muuttaa curry mattomaksi funktioksi: uncurry :: (a -> b -> c) -> ((a, b) -> c) uncurry f (p, q) = f p q Nyt esimerkiksi (uncurry (+)) (2, 3) sievenee normaalimuotoon 5. Funktiosidontaa voi käyttää myös let-lausekkeissa. 2.7.4 Hahmosidonta Toinen yleistys vakionmäärittelylle on hahmonsidonta, joka on syntaktisesti samanlainen kuin yhden parametrin funktiosidonta paitsi että funktionnimeä ei ole. Hahmossa esiintyvät muuttujat tulevat näin määriteltyä vakioiksi ja niiden arvo on se, mikä hahmon ja vartioimien mukaan niille kuuluu. Jos vartioimien valitsema lauseke ei sovi (osittain tai kokonaan) hahmoon, kaikkien hahmossa mainittujen muuttujien arvoksi tulee. Itse määrittely ei varsinaisesti epäonnistu koskaan. Esimerkki 16 Hahmonsidonnoissa (x:y:[]) = 3:[] (a:b:[]) = 3:4:[] muuttujien x ja y arvoksi tulee, muuttujan a arvoksi tulee 3 ja muuttujan b arvoksi tulee 4. Myös hahmonsidontaa voi käyttää let-lausekkeessa. 2.7.5 Where-määrittelyt Paikallisia määrittelyjä voi let-lausekkeen lisäksi tehdä myös where-lisäkkeellä, joka koostuu where-avainsanasta ja sen jälkeen aaltosulkeisiin kirjoitetuista, puolipisteillä erotetuista vakionmäärittelyistä, funktiosidonnoista ja hahmosidonnoista. Where-lisäkettä voidaan käyttää kaikissa sellaisissa yhteyksissä, missä käytetään hahmoja. Where-lisäke sijoitetaan viimeisen hahmoon liittyvän lausekkeen perään, ja siinä annetut määrittelyt näkyvät kaikissa kyseiseen
2.7. KOHTI AITOA HASKELLIA 27 hahmoon liittyvissä vartioimissa ja lausekkeissa. Where-lisäkkeessä voi käyttää tavalliseen tapaan sisennystekniikkaa. Esimerkki 17 Seuraava funktio ratkaisee enintään toista astetta olevan yhtälön ax 2 + bx + c = 0 reaalijuuret, kun sille annetaan kertoimet a, b ja c: roots :: Double -> Double -> Double -> [Double] roots a b c a == 0 && b == 0 && c == 0 = error "Ääretön ratkaisujoukko" a == 0 && b == 0 = [] -- ei ratkaisuja a == 0 && b /= 0 = [c / b] e < 0 = [] -- kompleksijuuret e == 0 = [-b/d] otherwise = [(-b-r)/d, (-b+r)/d] where r = sqrt e d = 2*a e = b*b - 4*a*c Huomautus 1 Huomaa, kuinka r:n määrittely ei johda virheeseen, vaikka e olisikin negatiivinen. Tämä johtuu siitä, että tällaisessa tapauksessa r:ää ei käytetä missään, ja sen -arvoa ei siis tarjota minnekään. Huomautus 2 Edellisessä esimerkissä näkyy myös kolme varuskirjastoon Prelude kuuluvaa vakiota: error :: String α sqrt :: Double Double otherwise :: Bool Funktiovakio error ottaa parametrinaan virheilmoituksen ja palauttaa aina :n. Haskellin toteutukset osaavat tunnistaa errorin tuottaman :n, ja näyttävät argumenttimerkkijonon virheilmoituksessa. Funktiovakio sqrt laskee parametrinsa neliöjuuren. Vakio otherwise on synonyymi koostimelle True. 2.7.6 Tyyppiluokat Ydin-Haskellin käyttämä Hindleyn ja Milnerin tyyppijärjestelmä on monessa suhteessa mainio, mutta sillä on yksi huomattava haittapuoli. Mikä esimerkiksi on (+)-operaattorin tyyppi? Aiemmin totesimme, että se on Integer Integer Integer. Se ei ole kuitenkaan tyydyttävä vastaus. Yllä esimerkissä 17
28 LUKU 2. OHJELMOINTI LASKENTANA sitä käytettiin Double-tyyppisten lukujen yhteenlaskuun. Onko sen tyyppi sitten α α α, sillä se on ainoa Ydin-Haskellin tyyppi, joka sopii molempiin yhteyksiin? Ei voi niinkään ajatella, sillä silloinhan True + False olisi mahdollinen lauseke. 2 Kyse on siitä, että (+) on kuormitettu (overloaded) eli ad hoc -polymorfinen operaattori. Oikeastaan kyse on eri funktioista, joilla on vain sama nimi. Mikä funktio tarvitaan, selviää argumenttityypeistä. Haskellissa kuormitukseen käytetään tyyppiluokkia. Operaattorin (+) oikea tyyppi on α. Num α α α α. Tämä luetaan seuraavasti: operaattorin (+) tyyppi on, kun α on mikä tahansa Numluokkaan kuuluva tyyppi, α α α. Tässä siis rajoitutaan tarkastelemaan vain sellaisia tyyppejä α, joilla on joitakin luvuilta odotettavia ominaisuuksia. Koneelle kirjoitettaessa osuus α. jätetään pois. Vastaavasti operaattorin (==) tyyppi on α. Eq α α α Bool. Tässä vaaditaan, että tyypin α arvoja voi mielekkäästi verrata; esimerkiksi funktiotyypit ovat sellaisia, ettei niiden arvojen yhtäsuuruutta ole järkevää koneen testata. Tyyppiluokkaan kuuluvilta tyypeiltä vaadittavat ominaisuudet määritellään tyyppiluokan määrittelyssä: class Eq a where (==), (/=) :: a -> a -> Bool Tämä sanoo, että tyyppiluokkaan Eq kuuluvalla tyypillä a toimii operaattorit (==) ja (/=), joiden tyyppi on α. Eq α α α Bool. Operaattoreita (==) ja (/=) sanotaan tyyppiluokan Eq metodeiksi. Eräs hyvin yleinen ja hyödyllinen tyyppiluokka on Show, jolla on useita metodeja, mutta niistä tärkein on ehdottomasti show :: α. Show α α String. Funktio show ottaa minkä tahansa Show-luokkaan kuuluvan tyypin arvon ja muuttaa sen ihmisen luettavaksi merkkijonoksi 3. Esimerkki 18 Prelude> show (3:4:5:6:9:[]) "[3,4,5,6,9]" Prelude> show (Just True) "Just True" Muita tarpeellisia tyyppiluokkia ovat Ord, jonka metodeita ovat vertailuope- 2. Jättäkäämme nyt huomiotta Boolen algebra... 3. Se on vieläpä mahdollista muuttaa takaisin kyseisen tyypin arvoksi, jos tyyppi kuuluu tyyppiluokkaan Read.
2.7. KOHTI AITOA HASKELLIA 29 raattorit (< yms.), Enum, jonka metodeita ovat succ ja pred, sekä Bounded, jonka metodeita ovat vakiot minbound ja maxbound. Itse määritelty algebrallinen tietotyyppi on mahdollista saattaa jonkin tietyn tyyppiluokan jäseneksi käyttämällä deriving-lisäkettä. Tämä lisäke kirjoitetaan tyyppimäärittelyn loppuun; se alkaa avainsanalla deriving, jonka jälkeen tulee sulkeissa pilkuilla erotettuna luettelo tyyppiluokista, joihin tyypin halutaan kuuluvan. Tällöin kääntäjä generoi vakiomuotoisen määrittelyn kyseisten tyyppiluokkien metodeille. Deriving-lisäke toimii vain tyyppiluokilla Eq, Ord, Enum, Bounded, Show ja Read ja lisäksi kaikkien niiden tyyppien, jotka esiintyvät kyseisessä tyypinmäärittelyssä, tulee kuulua niihin tyyppiluokkiin, jotka mainitaan deriving-lisäkkeessä. Käytännössä kannattaa opetella liittämään deriving (Eq, Ord, Show) kaikkiin omiin tyypinmäärittelyihin, ellei ole erityistä syytä toimia toisin. Yleinen tapa määritellä jokin tyyppi (kyseessä voi olla kirjastotyyppikin) jonkin tyyppiluokan jäseneksi on käyttää instance-määrittelyä. Siinä annetaan kyseisen tyyppiluokan metodien määrittelyt. Esimerkiksi: data BinarySearchTree key elt = BTNode (BinarySearchTree key elt) (key, elt) (BinarySearchTree key elt) BTNone bttolist :: BinarySearchTree key elt -> [(key, elt)] bttolist BTNone = [] bttolist (BTNode l kep r) = bttolist l ++ [kep] ++ bttolist r instance (Eq key, Eq elt) => Eq (BinarySearchTree key elt) where a == b = (bttolist a) == (bttolist b)