2.4. NORMAALIMUOTO, POHJA JA LASKENTAJÄRJESTYS 13 Toisinaan voi olla syytä kirjoittaa α- tai β-kirjain yhtäsuuruusmerkin yläpuolelle kertomaan, mitä muunnosta käytetään. Esimerkki 4 1. (λx.x)y β = y 2. (λx.λy.xy)y α = (λx.λz.xz)y β = λz.yz. (Jos kiellosta huolimatta olisi tehty β-muunnos saman tien, olisi tulokseksi tullut λy.yy.) Edellä esitetty λ-laskento on ns. puhdas λ-laskento. Käytännössä λ-laskennossa käytetään myös δ-muunnosta. Siihen liittyen lausekkeiden määritelmään lisätään aritmeettiset ynnä muut sellaiset lausekkeet. Delta-reduktio sanoo, että mikä tahansa δ-redex eli sellainen lauseke, joka sopii jonkin annetun määritelmäyhtälön (kuten ne, jotka yllä aiemmin annettiin) vasemmaksi puoleksi, voidaan korvata ko. yhtälön oikealla puolella. Jos useampi yhtälö sopii, niistä valitaan se, jonka vasen puoli on spesifein. Delta-abstraktio on sama prosessi toiseen suuntaan; δ-muunnoksella tarkoitetaan näitä molempia. Delta-muunnos siis käytännössä mahdollistaa perinteisen aritmeettisen laskennan: Esimerkki 5 1. (λx.x + 1) 3 β = 3 + 1 δ = 4. 2. (λx.λy.x + y) 3 4 β = (λy.3 + y) 4 β = 3 + 4 δ = 7. Tässä vaiheessa ehkä huomaakin jo, mistä λ-laskennossa on kyse: se on funktiolaskentaa puhtaimmillaan. Applikaatiolauseke edustaa funktion käyttöä (funktion kutsua ), kun taas abstraktio luo nimettömän funktion. Itse asiassa kaikki aiemmin asetetut määritelmät voitaisiin kirjoittaa uusiksi, jos niin haluttaisiin: voidaan kirjoittaa myös nelio x = x x nelio = λx.x x 2.4 Normaalimuoto, pohja ja laskentajärjestys Lausekkeen normaalimuodolla tarkoitetaan sellaista muotoa, johon ei voi soveltaa β-reduktiota eikä δ-reduktiota. Kun laskennossa pyritään saamaan pelkkä luku vastaukseksi, λ-laskennossa pyritään löytämään normaalimuoto. Valitettavasti kaikilla lausekkeilla ei ole normaalimuotoa.
14 LUKU 2. OHJELMOINTI LASKENTANA Esimerkki 6 Lausekkeella (λx.xxx)(λx.xxx) ei ole normaalimuotoa, sillä (λx.xxx)(λx.xxx) β = (λx.xxx)(λx.xxx)(λx.xxx). Otetaan käyttöön uusi lauseketyyppi, (luetaan: pohja). Merkitään sillä sellaisen laskun tulosta, joka ei koskaan päädy normaalimuotoon. Toisin sanoen edustaa päättymätöntä laskua. Sitä käytetään myös merkitsemään virheellisen laskun, esimerkiksi nollalla jakamisen, tulosta. Funktiot on tapana jakaa kahteen luokkaan sen mukaan, miten ne käyttäytyvät pohjan suhteen. Jos f = pätee, f on tiukka (strict) funktio. Jos f pätee, f on väljä (non-strict) funktio. Funktion väljyyden tai tiukkuuden usein määrää se, missä järjestyksessä laskut tehdään. Esimerkiksi lausekkeella (λx.y)((λx.xxx)(λx.xxx)) on normaalimuoto y, mutta se, että saavutetaanko se, riippuu siitä, missä järjestyksessä muunnokset suoritetaan. Kaksi järjestystä vaativat erityisen huomion: Normaalijärjestyksessä valitaan uloin, vasemmanpuolisin β- tai δ-redex ja sovelletaan siihen β- tai δ-reduktiota. Applikaatiivisessa järjestyksessä valitaan sisin, vasemmanpuolisin β- tai δ- redex ja sovelletaan siihen β- tai δ-reduktiota. Applikatiivisen järjestyksen perusominaisuus on se, että siinä funktion argumentit lasketaan ennen kuin funktion arvoa aletaan selvittämään. Tällöin, jos argumentti on, koko funktion arvoksi muodostuu. Normaalijärjestys puolestaan laskee argumentit mahdollisimman myöhään, jolloin funktio, joka ei käytä argumenttiaan, voi saada muunkin arvon kuin, vaikka argumenttina olisikin. Itse asiassa normaalijärjestys löytää normaalimuodon, jos sellainen on olemassa. 2.5 Ydin-Haskell Rakennetaanpa nyt yllä kerättyjen ideoiden pohjalta uusi kieli, jota sanomme Ydin-Haskelliksi. Se on Haskell-ohjelmointikielen osa siinä mielessä, että sillä kirjoitetut pätkät ovat laillista Haskellia, mutta se on varsinaista Haskellia yksinkertaisempi ja käsinlaskettavaksi sopivampi. Varsinainen Haskell määritellään tavallisesti Ydin-Haskellin kaltaisen kernelin ympärille yksinkertaisin muunnossäännöin. Merkitsemme Ydin-Haskellin lausekkeiden redusointia kaksoisnuolella ( ). Ydin-Haskellilla laskettaessa noudatetaan normaalijärjestystä.
2.5. YDIN-HASKELL 15 2.5.1 Lausekkeet Ydin-Haskellissa on monta lauseketyyppiä. Käsitellään niistä kukin erikseen. Lausekkeissa sulkeita käytetään tavanomaiseen tapaan ilmaisemaan niiden rakennetta. Peruslausekkeet Peruslausekkeita ovat seuraavat: Eksplisiittisesti tyypitetty lauseke kirjoitetaan e :: t, missä e on lauseke ja t on tyyppilauseke. Tämä voidaan redusoida lausekkeeksi e. Pohja on. Sitä ei koneelle koskaan kirjoiteta eikä kone sitä koskaan tunnista, mutta käsin laskiessa se on hyödyllinen. Muuttujat alkavat pienellä kirjaimella ja voivat sisältää numeroita, kirjaimia, alaviivoja ja heittomerkkejä ( ). Muuttujia ovat esimerkiksi pi, count ja n. Operaattoreita ovat muuttujat takahipsujen sisään kirjoitettuna (esim. lisaa ) sekä symbolijonot (esimerkiksi + tai ++), jotka eivät ala kaksoispisteellä. Lausekkeina operaattoreita voidaan käyttää kirjoittamalla ne sulkeisiin. Koostimet alkavat isolla kirjaimella ja voivat sisältää numeroita, kirjaimia, alaviivoja ja heittomerkkejä ( ). Koostimia ovat esimerkiksi Cons, True ja EmptyTree. Koostinoperaattoreita ovat koostimet takahipsujen sisään kirjoitettuna (esim. Cons ) sekä kaksoispisteellä alkavat symbolijonot (esimerkiksi : tai : +). Lausekkeina koostinoperaattoreita voidaan käyttää kirjoittamalla ne sulkeisiin. Literaaleja ovat näkyviin kirjoitetut luvut ja merkkijonot, kuten 52, 99.2 ja "Roope Ankka". Abstraktio ja applikaatiot Lambda-abstraktiot ovat muotoa λx E, missä x on jokin muuttuja ja E on jokin Ydin-Haskellin lauseke. Koneelle kirjoitettaessa λ-kirjain esitetään kenoviivana (\) ja nuoli muodossa ->. Applikaatio on lauseke, jossa kaksi Ydin-Haskellin lauseketta kirjoitetaan peräkkäin. Applikaatio assosioi vasemmalle, joten E F G tarkoittaa (E F) G. Operaattoriapplikaatio on lauseke, jossa kaksi Ydin-Haskellin lauseketta erotetaan operaattorilla tai koostinoperaattorilla. Presedenssi ja assosiointi riippuvat käytetystä operaattorista.
16 LUKU 2. OHJELMOINTI LASKENTANA Let-lausekkeet Let-lausekkeet ovat muotoa let {x 1 = e 1 ; x 2 = e 2 ;... ; x n = e n } in e 0, missä x i ovat muuttujia ja e i ovat lausekkeita. Tapauksessa n = 1 let-lauseke let {x 1 = e 1 } in e 0 tarkoittaa samaa kuin lauseke (λx 1 e 0 ) e 1. Yleisemmin let-lauseke pitää ymmärtää joukoksi lausekkeessa e 0 voimassa olevia δ- muunnossääntöjä, jolloin lausekkeessa e 0 muuttujat x i ovat δ-redeksejä. Tämä mahdollistaa rekursiivisten määritelmien tekemisen. Esimerkki 7 let {lisaa = λx λy x + y} in lisaa 2 3 5 Koneelle kirjoitettaessa aaltosulut ja puolipisteet voidaan korvata sisennyksillä ja rivinvaihdoilla. Sisennysohjeet on luettavissa seuraavasta kuvauksesta, joka kertoo, kuinka aaltosulkeet ja puolipisteet lisätään sellaiseen, josta ne puuttuvat: Jos let-avainsanan jälkeen ei tule aukeavaa aaltosuljetta, avainsanan jälkeen tulevan sanasen (numero, kirjain, välimerkki tms, mutta ei tyhjämerkit eikä rivinvaihdot) sisennys muistetaan ja aukeava aaltosulje lisätään avainsanan perään. Tyhjät rivit jätetään huomiotta. Jos seuraavaa riviä on sisennetty saman verran, puolipiste lisätään sen alkuun. Jos sitä on sisennetty vähemmän, loppusulje lisätään ja tämä let-lausekkeen käsittely päättyy. Samoin käy, jos seuraavaa sanasta ei voida tulkita kuuluvan letlauseen in-avainsanaa edeltävään osaan. Esimerkki 8 Lauseke let x = 4 y = 5 z = 7 in x + y + z tarkoittaa samaa kuin let {x = 4; y = 5; z = 7} in x + y + z. Case-lauseke Case-lausekkeen tarkoituksena on valita jonkin lausekkeen e arvon rakenteen ja ominaisuuksien perusteella laskettava lauseke. Case-lauseke on muotoa case e of {p 1 m 1 ; p 2 m 2 ;... ; p n m n }, missä p i ovat hahmoja ja m i ovat muotoa g i1 e i1 g i2 e i2 g ik e ik. Arvon rakenne kuvataan hahmolla (pattern). Hahmoja on seuraavanlaisia:
2.5. YDIN-HASKELL 17 Jokerihahmo (_) on hahmo. Muuttuja on hahmo. Koostinhahmo muodostuu joko koostimesta ja nollasta tai useammasta sen perään kirjoitetusta hahmosta tai kahdesta hahmosta, joiden väliin on kirjoitettu koostinoperaattori. Literaali on hahmo. Väljä hahmo on tilde, jota seuraa hahmo. Hahmossa kukin muuttuja saa esiintyä enintään kerran. Kuten lausekkeissa, myös hahmoissa sulkeita käytetään ilmaisemaan hahmon rakenne. Esimerkki 9 Joitakin hahmoja: 1. x 2. Cons 2 3 3. Cons x (Cons 4 y) 4. 2 : 3 : 4 : x Case-lauseke lasketaan (redusoidaan) seuraavasti: 1. Ensin redusoidaan e normaalimuotoon tai :ksi. 2. Sitten katsotaan, mitkä hahmot p i sopivat e:hen. Hahmo sopii lausekkeeseen, jos niillä on sama rakenne eli jos molemmissa samat koostimet, koostinoperaattorit ja literaalit ovat samoissa paikoissa. Väljä hahmo, muuttujahahmo ja jokerihahmo sopivat mihin tahansa lausekkeeseen, myös :aan. 3. Sen jälkeen hylätään kaikki ne p i m i -parit, joissa hahmo p i ei sovi lausekkeeseen e. Jos yhtään paria ei jää jäljelle, koko lauseke redusoidaan :ksi. 4. Kussakin jäljelläolevassa m i :ssä jokainen p i :ssä esiintyvä muuttuja korvataan sitä vastaavalla e:n alilausekkeella. 5. Seuraavaksi tehdään seuraava järjestyksessä kullekin vartioimelle (guard) g ij : (a) Vartioin g ij redusoidaan. Jos tulokseksi tulee, koko lauseke redusoidaan :ksi. (b) Jos tulokseksi tulee True, koko case-lauseke redusoidaan vastaavaksi e ij :ksi. Myös case-lausekkeessa voidaan käyttää let-lausekkeesta tuttua sisennystekniikkaa. Esimerkki 10 Lasketaan yksi case-lauseke:
18 LUKU 2. OHJELMOINTI LASKENTANA case Cons (3 + 4) (5 2) of Cons x 6 x > 2 -> x + 2 True -> x + 3 Nil True -> 99 Cons x y x > 5 -> x + y y > 5 -> x - y case Cons 7 10 of Cons x 6 x > 2 -> x + 2 True -> x + 3 Nil True -> 99 Cons x y x > 5 -> x + y y > 5 -> x - y case Cons 7 10 of Cons x y x > 5 -> x + y y > 5 -> x - y case Cons 7 10 of Cons 7 10 7 > 5 -> 7 + 10 10 > 5-> 7-10 7 + 10 17 2.5.2 Tyypit Ydin-Haskell vaatii, että jokaisella lausekkeella on tyyppi, joka ilmaisee lausekkeen normaalimuodon rakennetyypin. Ydin-Haskellin tyypit ilmaistaan seuraavin tyyppilausekkein: () on tyyppilauseke. Tyyppikoostimet alkavat isolla kirjaimella ja voivat sisältää numeroita, kirjaimia, alaviivoja ja heittomerkkejä ( ). Tyyppikoostimia ovat esimerkiksi List, Bool ja BinaryTree. Tyyppimuuttujat alkavat pienellä kirjaimella ja voivat sisältää numeroita, kirjaimia, alaviivoja ja heittomerkkejä ( ). Tyyppimuuttujia merki-
2.5. YDIN-HASKELL 19 tään painetussa ja käsin kirjoitetussa materiaalissa usein pienillä kreikkalaisilla kirjaimilla. Jos Γ ja ovat tyyppilausekkeita, niin Γ on tyyppilauseke. Nuoli kirjoitetaan koneella ->. Nuoli assosioi oikealle, joten A B C tarkoittaa A (B C). Jos A on tyyppikoostin ja Γ 1,..., Γ n ovat tyyppilausekkeita, niin A Γ 1... Γ n on tyyppilauseke. Tyyppilausekkeidenkin rakenne ilmaistaan sulutuksella tavanomaiseen tapaan. Tyypit voivat olla monomorfisia tai polymorfisia. Polymorfisia ne ovat silloin, kun niissä esiintyy yksikin tyyppimuuttuja, monomorfisia muulloin. Monomorfiset tyypit ovat samoja, jos niillä on sama rakenne (samat tyyppikoostimet ja nuolet samoissa paikoissa). Polymorfiset tyypit (tai monomorfinen tyyppi ja polymorfinen tyyppi) voidaan samastaa, jos kumpikin voidaan muuttaa monomorfiseksi korvaamalla siinä esiintyvät tyyppimuuttujat tyyppilausekkeilla ja jos niistä tulee näin samat. Lausekkeen tyyppi on pääteltävä ja tarkastettava ennen kuin mitään redusointia tehdään. Tämä tapahtuu seuraavien sääntöjen avulla lukemalla lauseketta sisältä ulospäin: Lausekkeen e :: t tyyppi on t. Jos e:n tyyppi ei ole t, on tapahtunut tyyppivirhe ja lauseke hylätään. Lausekkeen tyyppi on α. Muuttujan tyyppi on α. Operaattorin, koostimen ja koostinoperaattorin tyyppi riippuu sen määritelmästä. Jos sitä ei ole määritelty, kyseessä on tyyppivirhe ja lauseke hylätään. Literaalin tyyppi riippuu literaalista (tässä vaiheessa voidaan olettaa kokonaislukujen tyypiksi Integer, liukulukujen tyypiksi Double ja merkkijonojen tyypiksi String). Olkoon lausekkeen e tyyppi u, ja olkoon siinä esiintyvän muuttujan x tyyppi t (päätelty e:tä tutkimalla). Tällöin abstraktion λx e tyyppi on t u.