main :: IO () main = interact (concatmap ((++"\n"). reverse). lines)

Samankaltaiset tiedostot
TIEA341 Funktio-ohjelmointi 1, kevät 2008

6 Algebralliset tietotyypit

tään painetussa ja käsin kirjoitetussa materiaalissa usein pienillä kreikkalaisilla

Algebralliset tietotyypit ym. TIEA341 Funktio ohjelmointi 1 Syksy 2005

TIEA341 Funktio-ohjelmointi 1, kevät 2008

5.2.5 Konstruktoriluokat Edellisessä esimerkissä määrittelimme oman tyyppiluokan Isqrt jonka jäsenet olivat tyyppejä (kuten Int, Integer, Word,...).

Tämän vuoksi kannattaa ottaa käytännöksi aina kirjoittaa uuden funktion tyyppi näkyviin, ennen kuin alkaa sen määritemää kirjoittamaan.

Monadeja siellä, monadeja täällä... monadeja kaikkialla? TIES341 Funktio ohjelmointi 2 Kevät 2006

TIEA341 Funktio-ohjelmointi 1, kevät 2008

TIEA341 Funktio-ohjelmointi 1, kevät 2008

5.5 Jäsenninkombinaattoreista

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Laajennetaan vielä Ydin-Haskellia ymmärtämään vakiomäärittelyt. Määrittely on muotoa

Lisää laskentoa. TIEA341 Funktio ohjelmointi 1 Syksy 2005

Tyyppiluokat II konstruktoriluokat, funktionaaliset riippuvuudet. TIES341 Funktio-ohjelmointi 2 Kevät 2006

Uusi näkökulma. TIEA341 Funktio ohjelmointi 1 Syksy 2005

Luku 3. Listankäsittelyä. 3.1 Listat

Tyyppejä ja vähän muutakin. TIEA341 Funktio ohjelmointi 1 Syksy 2005

Demo 7 ( ) Antti-Juhani Kaijanaho. 9. joulukuuta 2005

Se mistä tilasta aloitetaan, merkitään tyhjästä tulevalla nuolella. Yllä olevassa esimerkissä aloitustila on A.

815338A Ohjelmointikielten periaatteet Harjoitus 6 Vastaukset

ELM GROUP 04. Teemu Laakso Henrik Talarmo

Ohjelmoinnin perusteet Y Python

2.4 Normaalimuoto, pohja ja laskentajärjestys 2.4. NORMAALIMUOTO, POHJA JA LASKENTAJÄRJESTYS 13

Alkuarvot ja tyyppimuunnokset (1/5) Alkuarvot ja tyyppimuunnokset (2/5) Alkuarvot ja tyyppimuunnokset (3/5)

TIEA241 Automaatit ja kieliopit, syksy Antti-Juhani Kaijanaho. 30. marraskuuta 2015

Lisää pysähtymisaiheisia ongelmia

Tietorakenteet ja algoritmit - syksy

Algoritmit 2. Luento 2 Ke Timo Männikkö

815338A Ohjelmointikielten periaatteet Harjoitus 7 Vastaukset

Abstraktit tietotyypit. TIEA341 Funktio ohjelmointi 1 Syksy 2005

TIEA341 Funktio-ohjelmointi 1, kevät 2008

3. Muuttujat ja operaatiot 3.1

IDL - proseduurit. ATK tähtitieteessä. IDL - proseduurit

Vasen johto S AB ab ab esittää jäsennyspuun kasvattamista vasemmalta alkaen:

Algoritmit 1. Luento 3 Ti Timo Männikkö

Ydin-Haskell Tiivismoniste

ATK tähtitieteessä. Osa 3 - IDL proseduurit ja rakenteet. 18. syyskuuta 2014

Ohjelmoinnin peruskurssien laaja oppimäärä

Luku 4. Tietorakenteet funktio-ohjelmoinnissa. 4.1 Äärelliset kuvaukset

Sisällys. 3. Muuttujat ja operaatiot. Muuttujat ja operaatiot. Muuttujat. Operaatiot. Imperatiivinen laskenta. Muuttujat. Esimerkkejä: Operaattorit.

Luku 5. Monadit. 5.1 Siirrännän ongelma

TIEA241 Automaatit ja kieliopit, kesä Antti-Juhani Kaijanaho. 26. kesäkuuta 2013

Tietorakenteet ja algoritmit Johdanto Lauri Malmi / Ari Korhonen

Algoritmit 2. Luento 2 To Timo Männikkö

Luku 2. Ohjelmointi laskentana. 2.1 Laskento

Algoritmit 2. Luento 7 Ti Timo Männikkö

8.5 Takarekursiosta. Sanoimme luvun 8.3 foldl -esimerkissämme että

14.1 Rekursio tyypitetyssä lambda-kielessä

Ohjelmoinnin peruskurssien laaja oppimäärä

Hahmon etsiminen syotteesta (johdatteleva esimerkki)

5.3 Laskimen muunnelmia 5.3. LASKIMEN MUUNNELMIA 57

T Syksy 2004 Logiikka tietotekniikassa: perusteet Laskuharjoitus 7 (opetusmoniste, kappaleet )

Laiska laskenta, korekursio ja äärettömyys. TIEA341 Funktio ohjelmointi Syksy 2005

815338A Ohjelmointikielten periaatteet Harjoitus 3 vastaukset

Ohjelmoinnin peruskurssien laaja oppimäärä

Esimerkki: Laskin (alkua) TIEA341 Funktio ohjelmointi 1 Syksy 2005

Ohjelmoinnin peruskurssien laaja oppimäärä

Tutoriaaliläsnäoloista

ja λ 2 = 2x 1r 0 x 2 + 2x 1r 0 x 2

Ohjelmoinnin peruskurssien laaja oppimäärä

Algoritmit 1. Luento 7 Ti Timo Männikkö

TIEA241 Automaatit ja kieliopit, syksy Antti-Juhani Kaijanaho. 3. lokakuuta 2016

Haskell ohjelmointikielen tyyppijärjestelmä

Rekursiolause. Laskennan teorian opintopiiri. Sebastian Björkqvist. 23. helmikuuta Tiivistelmä

815338A Ohjelmointikielten periaatteet Harjoitus 2 vastaukset

Ohjelmoinnin perusteet Y Python

Chapel. TIE Ryhmä 91. Joonas Eloranta Lari Valtonen

PERL. TIE Principles of Programming Languages. Ryhmä 4: Joonas Lång & Jasmin Laitamäki

1 Mitä funktionaalinen ohjelmointi on?

Ohjelmoinnin peruskurssien laaja oppimäärä

11/20: Konepelti auki

Ohjelmoinnin peruskurssien laaja oppimäärä

Jäsennys. TIEA341 Funktio ohjelmointi 1 Syksy 2005

Perusteet. Pasi Sarolahti Aalto University School of Electrical Engineering. C-ohjelmointi Kevät Pasi Sarolahti

Perusteet. Pasi Sarolahti Aalto University School of Electrical Engineering. C-ohjelmointi Kevät Pasi Sarolahti

Ohjelmoinnin peruskurssien laaja oppimäärä

Ohjelmoinnin perusteet Y Python

etunimi, sukunimi ja opiskelijanumero ja näillä

Harjoitustyön testaus. Juha Taina

Zeon PDF Driver Trial

ITKP102 Ohjelmointi 1 (6 op)

ITKP102 Ohjelmointi 1 (6 op)

13. Loogiset operaatiot 13.1

System.out.printf("%d / %d = %.2f%n", ekaluku, tokaluku, osamaara);

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Ehto- ja toistolauseet

TIEA241 Automaatit ja kieliopit, kevät 2011 (IV) Antti-Juhani Kaijanaho. 19. tammikuuta 2012

811120P Diskreetit rakenteet

Python-ohjelmointi Harjoitus 5

Datatähti 2019 loppu

TIES542 kevät 2009 Tyyppijärjestelmän laajennoksia

System.out.printf("%d / %d = %.2f%n", ekaluku, tokaluku, osamaara);

815338A Ohjelmointikielten periaatteet Harjoitus 4 vastaukset

Ohjelmoinnin perusteet Y Python

11.4. Context-free kielet 1 / 17

Imperatiivisen ohjelmoinnin peruskäsitteet. Meidän käyttämän pseudokielen lauseiden syntaksi


Tietotekniikan valintakoe

Koottu lause; { ja } -merkkien väliin kirjoitetut lauseet muodostavat lohkon, jonka sisällä lauseet suoritetaan peräkkäin.

Transkriptio:

saa syötteenään oletussyötevirran stdin koko (jäljellä olevan) sisällön laiskasti luettavana merkkijonona antaa tulosteensa toisena merkkijonona jota se voi rytmittää rivinvaihdoilla. Esimerkiksi: module Main(main) where main :: IO () main = interact (concatmap ((++"\n"). reverse). lines) Mutkikkaammat I/O-operaatiot kuten esimerkiksi tiedostojen käsittely löytyvät vakiokirjastosta System.IO. Esimerkiksi usein halutaan oletustulosvirta stdout sellaiseksi, ettei sitä puskuroidakaan. Siihen tämä kirjasto tarjoaa kutsun hsetbuffering stdout NoBuffering jonka voi sijoittaa sellaisen ohjelman mainin alkuun. Poikkeuksista Poikkeuksia (eception) on hankala sovittaa yhteen puhtaan funktionaalisen ohjelmoinnin kanssa: Se milloin poikkeus käsitellään olettaa, että ohjelmoija tietäisi milloin jotakin tapahtuu laskennan kuluessa mutta puhtaassa funktionaalisessa ohjelmoinnissahan ohjelmoija tietää korkeintaan että se joskus tapahtuu... Haskellin vakiokirjasto System.IO.Error tarjoaakin yksinkertaisen poikkeusmekanismin erilaisten I/O-virheiden käsittelyyn koska Monadissa IO on tällainen järjestys ensin lasketaan tämä, ja sitten vasta tuo... Toteutuksen ghc mukana tulee myös sen oma toinenkin kirjasto Control.Eception jolla poikkeuksia voi nostaa muuallakin kuin Monadissa IO mutta nekin voi käsitellä vain siellä tämän järjestyksen vuoksi. Vakiokirjaston I/O-poikkeusmekanismi koostuu tyypistä IOError eli jokin I/Ovirhe sekä funktioista ioerror :: IOError -> IO a joka nostaa parametrina ilmoitetun I/O-poikkeuksen catch :: IO a -> (IOError -> IO a) -> IO a jonka ensimmäinen parametri on I/O-tyyppinen lauseke (kuten esimerkiksi jokin do-lauseke) jonka laskenta voi aiheuttaa I/O-poikkeuksia. Jos niitä ei aiheudu, niin catchin arvoksi tulee tämän laskennan antama arvo. 104

jälkimmäinen parametri on funktio, jota kutsutaan, jos näin käy. Se saa parametrinaan sen aiheutuneen poikkeuksen. Se voi käsitellä kyseisen poikkeustilanteen palauttamalla jonkin arvon, josta tulee tämän catchin arvo. Jos se ei osaakaan käsitellä tilannetta, niin sen pitääkin nostaa sama poikkeus uudelleen, jolloin tilanteen käsittelyvastuu siirtyykin seuraavalle ulommalle catchille. Jos mikään catch ei osaakaan sitä käsitellä, niin silloin koko mainin laskenta keskeytyy suoritusaikaiseen virheeseen. try :: IO a -> IO (Either IOError a) joka koettaa suorittaa parametrinaan saamansa laskennan. Jos sen arvo on Right niin tämä laskenta onnistui virheettä ja on sen lopputulos Left e niin silloin tämä laskenta johtikin poikkeukseen e. Otetaan sitten esimerkiksi I/O-ohjelmoinnista nelilaskin, jossa pidetään muistissa yhtä liukulukua käyttäjä voi antaa komentonaan lisää/vähennä/kerro/jaa se luvulla... tämän komentorivin mahdolliset virheet käsitellään tyhjä komentorivi lopettaa ohjelman suorituksen. module Main(main) where import System.IO import System.IO.Error main :: IO () main = do hsetbuffering stdout NoBuffering laskin 0.0 laskin :: Double -> IO () laskin muisti = do putstr $ "Muistissani on: " ++ show muisti ++ "\nkomentosi? " komentorivi <- getline case komentorivi of "" -> do putstrln "Kiitos!" return () (komento:rivi) -> case lookup komento komennot of Nothing -> do putstrln $ "Tunnen vain seuraavat laskutoimitukset: " ++ map fst komennot ++ "!" laskin muisti 105

Just operaattori -> do arvo <- try $ readio rivi case arvo of Right luku -> laskin $ muisti operaattori luku Left _ -> do putstrln "Tajuan vain liukulukuja!" laskin muisti komennot :: [(Char,Double->Double->Double)] komennot = [( +,(+)),( -,(-)),( *,(*)),( /,(/)) ] 7.3 Maybe monadina Aiemmin näimme, miten Maybe on Monadi: instance Monad Maybe where (Just ) >>= k = k Nothing >>= k = Nothing return = Just fail s = Nothing Tarkastellaan sitä tässä valossa, että lausekkeen p >>= k voi lukea myös suorita ensin laskenta p ja sovella sitten sen tulokseen funktiota k. Silloin näemme uuden tulkinnan Maybelle: Jos laskenta p sai jonkin tuloksen niin sovella siihen funktiota k, mutta jos se ei saanutkaan mitään tulosta, niin koko loppulaskentakaan ei saa mitään tulosta koska siitä alkaen jokaisen (>>=)-operaattorin vasen parametri on aina Nothing. Silloin siis jokainen Maybe-tyyppisen do-lauseen hahmo <- kutsuttava ohitetaan kokonaan, jos jokin sitä edeltävistä tällaisista lauseista on jo tuottanut arvon Nothing siinä kutsuttava laskenta tuottaakin sellaisen arvon, joka ei olekaan muotoa olekaan muotoa Just hahmo. Silloin siis tästä alkaen kaikki lauseet ohitetaan. Siten tässä valossa Maybe antaakin tyypin sellaiselle laskennalle joka päättyy epäonnistumiseen jos yksikin siinä kutsuttava laskenta epäonnistuu. 106

7.4 Lista monadina Olemme nähneet jo aiemmin, miten listat ovat Monadi: instance Monad [] where m >>= k = concat (map k m) return = [] fail s = [] Tarkastellaan sitäkin tässä valossa, että lausekkeen p >>= k voi lukea myös suorita ensin laskenta p ja sovella sitten sen tulokseen funktiota k. Nyt sen koko laskenta etenee seuraavasti: 1. Laskenta p tuottaa tuloksenaan listan m eli [ 1, 2, 3,...]. 2. Sen jokaiseen alkioon i sovelletaan (map) funktiota k. 3. Jokainen sovellus k i tuottaa tuloksenaan jonkin listan [y 1 i, y 2 i, y 3 i,...]. 4. Yhdistetään (concat) nämä listat yhdeksi listaksi k 1 ++ k 2 ++ k 3 ++... 5. Sitten ympäröivässä listamonadilausekkeessa >>=k sovelletaan seuraavaa funktiota k jokaiseen tämän listan alkioon y j i, jne. Vaihdetaan näkökulmaa kokonaisista listoista niiden yksittäisiin alkioihin: Ajatellaan, että alkio i edustaisi jotakin tilaa, jossa koko laskenta voisi olla tällä hetkellä. Silloin k i tuottaa sille monta eri mahdollista seuraavaa tilaa y j i. Tässtä näkökulmasta k on siis epädeterministinen seuraava laskenta-askel. Tässä valossa listamonadi ja sen do-syntaksi kuvaavat epädeterministisen laskennan kaikkien eri vaihtoehtojen seuraamiseen. Deklaratiivisesti yksi tällainen epädeterministinen askel hahmo <- kutsuttava voidaan lukea valitse mikä tahansa hahmo on sopiva vaihtoehto kutsuttava n laskennan antamista mahdollisuuksista. Tässä luennassa <- on. 107

Proseduraalisesti se luetaan kokeile jokaista näistä vaihtoehdoista siinä järjestyksessä jossa kutsuttava ne tuottaa. Tässä luennassa tuo deklaratiivisuus toteutetaan syvyyshakuna (depth-first search). Se on epätäydellistä koska se juuttuu ikuiseen silmukkaan, jos sellaiseen johtava väärä vaihtoehto tulee tässä kokeilujärjestyksessä ennen oikeaa vaihtoehtoa. tehokkaasti toteutettavissa erityisesti muistinkäytöltään. Haskellin laiskat listat hoitavat sen automaattisesti ohjelmoijatta. Aiemmassa esimerkissämme säännöllisistä lausekkeista käytimme juuri tätä samaa ideaa ja näimme erikseen vaivaa sen pysähtymisen takaamiseksi. Olemme nähneet aiemmin myös listakoosteet, joiden syntaksi oli hyvin lähellä dosyntaksia: 1. Listakoosteissa sallittiin vaihe ena Bool-tyyppinen lauseke, jota do-syntaksissa ei sallita. 2. Toisaalta do-syntaksissa sallittiin lause ena mielivaltainen kyseisen Monadityypin lauseke, kun taas listakoosteissa ei sallittu vaihe ena pelkkää listalauseketta ilman edeltävää osaa hahmo <-. Tämä ei tietenkään ole sattumaa vaan suunniteltua: Listakoosteet ovatkin yleisen Monadisen do-syntaksin erikoistapaus listamonadille. Eron 1 voi ymmärtää tästä näkökulmasta: Hylkää tämä nykyinen epädeterministinen laskentahaara ellei tuo ehto q ole totta on luonteva operaatio juuri listamonadissa mutta ei muissa. Tällaisen ehdontestausvaiheen q voi ilmaista listamonadin do-syntaksissa vaikkapa () <- if q then return () else fail Jos listakoosteen ehdontestausvaihe eet q on näin muunnettu tuottaviksi vaihe iksi, niin silloin se voidaan muuntaa suoraan do-syntaksiin: listakoosteesta [lauseke osa 1, osa 2, osa 3,. osa r ] do-syntaksiin do osa 1 osa 2 osa 3. osa r return lauseke 7.5 Tilaperustaisesta ohjelmoinnista monadina Lähdimme kurssin alussa siitä, että matemaattinen funktio on mahdollisimman tiukka rajapintakäsite ohjelmointiin: sisään menee informaatiota vain parametreissa 108

ulos ulos tulee informaatiota vain tuloksessa. Voidaan ajatella ja ohjelmoida sellaisia funktioita, jotka näiden funktionaalisten rajapintojensa sisällä ovatkin tilaperustaisia. Silloin vain täytyy pitää huoli, että tämä tila pysyy yksityisenä tällaisen funktion jokaisella käyttökerralla: Kun tällaisen funktion arvon laskenta alkaa, niin myös sen sisäinen tila alustetaan eikä jatketakaan siitä tilasta johon edellinen laskenta päättyi. Jos tällaisen funktion kahta eri arvoa laskettaisiin rinnakkain, niin kumpikin muokkaisi omaa yksityistä tilaansa. ghc tarjoaa tähän tarkoitukseen oman kirjastonsa Control.Monad.ST joka ei siis ole Haskell-standardikirjasto edes Haskell-kielistandardin mukainen, vaan käyttää ghc:n omia laajennuksia tyyppijärjestelmään. Näillä laajennuksilla Haskellin tyypitys pystyy takaamaan, että rajapinnat säilyvät funktionaalisina, vaikka sisäisesti laskenta onkin tilallista. Jos ohjelmoija yrittää vahingossa tai tahallaan rikkoa koodissaan näitä rajapintoja, niin hän siis saakin käännösaikaisen tyyppivirheilmoituksen. Valitettavasti nämä ilmoitukset voivat olla vaikeaselkoisia... Tämän Monadin nimenä onkin STate eli tila. Otetaan esimerkiksemme seuraava ohjelmointiongelmaa: Saamme listan pareja (,y) jotka tarkoittavat että alkioiden ja y pitää kuulua samaan joukkoon. Mitkä nämä joukot ovat, kun alkiot pidetään erillään, ellei mikään tällainen pari vaadi niiden kuuluvan yhteen? Se vaatii tietorakenteen, jossa on seuraavat operaatiot: initial joka alustaa sen siten, että jokainen alkio on aluksi yksin omassa joukossaan. union joka yhdistää ne joukot, joissa annetut alkiot ja y nyt ovat. Siitä käytetään myös nimeä merge. find palauttaa sen joukon edustajan, johon alkio z nyt kuuluu. Joukon edustaja on jokin sen alkioista. Sitä käytetään koko joukon nimenä. Kun esimerkiksi kysytään ovatko alkiot ja y jo nyt samassa joukossa niin re ratkaistaan kysymällä, onko alkion joukon edustaja sama kuin alkion y joukon edustaja. Tämä tietorakenne tunnetaankin nimellä union-find tai merge-find. 109

Tämä vaikuttaa sellaiselta ohjelmointiongelmalta, jonka imperatiivinen ratkaisu on funktionaalista nopeampi: Koska imperatiivisessa ratkaisussa voidaan käyttää uudelleensijoitusta, niin siellä voidaan tiivistää hakupolku alkiosta sen edustajaan, jotta seuraavat kyselyt sujuvat nopeammin. Tällöin saadaan ratkaisu, joka toimii askeleessa, jossa O((m + n) α(m + n)) m on alkioiden lukumäärä n on operaatioiden union ja find lukumäärä α on erittäin hitaasti kasvava funktio. Se on aiemmin näkemämme Ackermannin funktion (joka siis kasvoi erittäin nopeasti) käänteisfunktion sukulainen. Käytännössä α(m + n) > 5 vasta kun m + n > alkeishiukkasten arvioitu lukumäärä koko maailmankaikkeudessamme... Funktionaalisessa onjelmoinnissa tämä polun tiivistäminen ei ole mahdollista, ja ainakin yksinkertaiset tietorakenneratkaisut tarvitsevat funtion α tilalla logaritmin. Imperatiivisessa ratkaisussa on oleellisesti taulukko mem, jossa alkiosta z 0 alkava polku mem[z 0 ] mem[z 1 ] mem[z 2 ] z 0 z 1 z 2 mem[zp] z p+1 johtaa alkion joukon edustajaan z p+1. Kun kysytään operaatiolla find alkion joukon edustajaa, niin samalla tämä polku tiivistetään muotoon mem[z 0 ] = mem[z 1 ] = mem[z 2 ] = = mem[z p ] = z p+1 jolloin kaikkien näiden muidenkin alkioiden z 1, z 2, z 3,...,z p 1 operaatiolla find nopeutuvat. Operaatiota union voidaan taas tehostaa seuraavasti: Pidetään yllä jokaisessa edustajassa tietoa rank joka olisi näistä mem-linkeistä koostuvan puun korkeus ilman polkujen tiivistämistä. Tämän tiedon avulla yhdistetään aina matalamman puun juuri korkeamman puun juuren alipuuksi. Nämä juuret eli edustajat antaa siis operaatio find. Monadissa ST voimme luoda tällaisen taulukon mem ja käyttää sitä: Operaatio newarray_ :: (MArray a e m, I i) => (i, i) -> m (a i e) luo taulukon tälle indeksivälille sellaisessa monadissa m jossa sellaiset sallitaan. 110

ST on siis yksi sellainen m. IO on toinen sellainen m. Tyyppiluokka I on ne tyypit, joita voi käyttää taulukkoindekseinä. Operaatio readarray :: (MArray a e m, I i) => a i e -> i -> m e lukee tällaisen taulukon arvon annetusta indeksistä. Operaatio writearray :: (MArray a e m, I i) => a i e -> i -> e -> m () kirjoittaa tällaisen taulukon annnettuun indeksiin annetun arvon. module UnionFind where import Control.Monad import Control.Monad.ST import Data.Array.ST import Data.Word unions :: (I t) => [(t,t)] -> [[t]] unions ys = let lo = minimum sys hi = maimum sys sys = s ++ ys (s,ys) = unzip ys in runst $ do mem <- initial lo hi sequence_ $ map (uncurry $ union mem) ys sequence_ $ map (find mem) $ range (lo,hi) memz <- getassocs mem parties <- parts lo hi sequence $ map ( \ (i,it) -> let j = case it of Rank _ data Item t = Rank Word Ranked t memz groupies <- getelems parties return $ map reverse $ filter (not. null) groupies -> i Ranked k -> k in do jt <- readarray parties j writearray parties j $ i:jt) 111

deriving (Show) type UF s t = STArray s t (Item t) parts :: (I t) => t -> t -> ST s (STArray s t [t]) parts lo hi = newarray (lo,hi) [] initial :: (I t) => t -> t -> ST s (UF s t) initial lo hi = do mem <- newarray_ (lo,hi) sequence_ $ map (flip (writearray mem) (Rank 0)) $ range (lo,hi) return mem find :: (I t) => UF s t -> t -> ST s t find mem = let finder i = do here <- readarray mem i case here of Rank r -> return i Ranked j -> do k <- finder j writearray mem i $ Ranked k return k in finder union :: (I t) => UF s t -> t -> t -> ST s () union mem y = do i <- find mem j <- find mem y when (i/=j) $ do Rank p <- readarray mem i Rank q <- readarray mem j if p<q then writearray mem i (Ranked j) else do writearray mem j (Ranked i) when (p==q) $ writearray mem i (Rank (p+1)) Funktionaalinen Haskell-ohjelma voi laskea tilaperustaisena määritellyn funktion tuloksen käyttämällä funktiota runst :: (forall s. ST s a) -> a joka suorittaa parametrinsa saamansa ST-laskennan alkusta loppuun saakka 112

palauttaa arvonaan sen lopputuloksen, jonka tyyppi on a. Siten tyyppi ST s a on tilaperustainen laskenta, jonka lopputulos on tyyppiä a. Siten ST on sellainen kaksiparametrinen tyyppikonstruktori, joka saadessaan ensimmäisen parametrinsa s tuottaa Monadin ST s. Useimpiin Monadeihin liittyy tällainen suorita -funktio, jolla funktionaalisesta koodista käsin voi ajaa Monadisen laskennan. Kääntäen, Monadissa IO ei ole tällaista funktiota runio koska sehän juuri mahdollistaisi I/O-toiminnat keskellä funktionaalista koodia. Entä tuo tyyppiparametri s? Siinä on käytetty Haskellin laajennusta forall jota emme ole käsitelleet tarkemmin emmäekä tässäkään tutustu siihen syvällisesti. Sen intuitio on että s voi olla mikä tahansa tyyppi joten kääntäen siitä ei voi olettaa yhtään mitään, eli se on erittäin abstrakti. Tässä yhteydessä tämän tyypin voi lukea hiekkalaatikko (sandbo) jonka sisällä tämä tilaperustainen laskenta toimii. Haskellin laajennettu tyyppijärjestelmä takaa, ettei tilaperustainen laskenta voi palauttaa mitään sellaista vastausta, jonka tyypissä esiintyisi tämä s. Siten se ei voi palauttaa esimerkiksi taulukkoa mem koska sen tyyppi on UF s t. Näin se turvaa tilan säilymisen yksityisenä jokaisessa funktion runst kutsussa. 7.6 Monadisen ohjelmoinnin periaatteista Jos haluaa kirjoittaa funktion f, joka käyttää monadin M palveluita (kuten monadin IO syötteenluku- ja tulostuspalveluita) ja palauttaa arvon tyyppiä τ, niin sen tulostyyppinä on M τ viimeinen lauseke do-notaatiossa on return e jossa lausekkeen e tyyppi on τ. Tälläisen funktion arvon a voi sitten laskea do-notaatiossa <- f parametrit jossa myös hahmon tyyppi on τ ja se nimeää arvon a. Funktion f parametri en tyypit kirjoitetaan funktion f tyyppiin tavalliseen tapaan. Jos jonkin parametri n tyyppi on M µ niin se on monadinen vastine korkeamman kertaluvun parametrille: Esimerkiksi funktion when :: (Monad m) => Bool -> m () -> m () 1. parametri on tavallinen totuusarvotyyppinen testi 113

2. parametri on laskenta, joka suoritetaan jos tämä testi on tosi muuten ei tehdä mitään, siksi tulostyyppi on (). Vakiokirjastot Prelude ja Control.Monad käyttävät tällaisia korkeamman kertaluvun monadisia parametreja määritelläkseen tällaisia uusia kontrollirakenteita kuten tämä when. Näissä kontrollirakenteissa voidaan yhdistellä esimerkiksi funktionaalista listankäsittelyä ja monadisia operaatioita: Esimerkiksi sequence_ :: Monad m => [m a] -> m () sequence_ = foldr (>>) (return ()) tekee kokonaisen listan esimerkiksi tulostuosoperaatioita (joiden tyyppi on IO ()) alusta loppuun saakka, koska sequence_ [p 1,p 2,p 3,...,p k ] on suoraan funktion foldr määritelmän nojalla p 1 >> (p 2 >> (p 3 >> (... >> (p k >> return ())...))) joka taas on sokeroimaton muoto lausekkeelle do p 1 p 2 p 3. p k return () Muutkin ohjelmointitehtävät joissa on luonteva käsite ensin tämä ja sitten tuo voidaan mallintaa monadeilla. Sellainen on esimerkiksi jäsentäminen (parsing). Aiempaa säännöllisten lausekkeiden esimerkkiämme voikin kehittää edelleen kirjastoksi monadisia jäsenninkombinaattoreita (monadic parser combinator). Silloin do 1 <- q 1 2 <- q 2 3 <- q 3. k <- q k return $ f 1 2 3... k 114

on yksi kielioppisääntö ensin tulee jotakin, jonka q 1 jäsentää, sitten jotakin jota q 2 jäsentää, sitten... ja nämä i ovat niitä vastaavat semanttiset semanttiset rakenteet kuten jäsennyspuut. Ne taas on luonteva esittää algebrallisilla tietotyypeillä. Näitä voi sitten yhdistellä (combine) sopivilla operaattoreilla kuten r < > s eli käytä joko jäsennintä r tai jäsennintä s. HackageDB sisältää kirjaston tällaisen Parsec. Yleisemmin monadisen ohjelmoinnin haittavaikutus on, että ne tartuttavat helposti koodia: Jos yksi osa koodista on Monadista, niin myös siinä kutsuttavien koodinosien pitää usein olla Monadisia, ja näin isosta osasta koodia tulee monadisesti monoliittista. Tämä johtuu siitä, että Monadinen tyyppi sanoo että tämä koodi voi tarvita tämän Monadin tarjoamia palveluita. Tästä näkökulmasta esimerkiksi ST s tarkoittaa tämä koodi voi tarvita uudelleensijoituslausetta. Koodin monoliittisuus tarkoittaakin usein sitä, että Monadia pidetään varmuuden vuoksi tarjolla ja sen voisi välttää miettimällä tarkemmin mitä palveluita oikeastaan milloinkin koodissaan tarvitsee. Monadikirjastot pyrkivät usein ehkäisemään tätä monoliittisyyttä tarjoamalla monadimuuntimia (monad transformers) joilla voi lisätä jonkin sisemmän monadin päälle kuorikerrokseksi toisen monadin. Tällöin jokaisessa kerroksessa on vain siinä tarpeelliset palvelut. 115

8 Laiskan laskennan teoriaa ja käytäntöä Tarkastellaan lopuksi hieman sitä, mistä Haskellin laiskassa laskennassa tarkemmin sanoen onkaan kyse. Tähän mennessä olemme puhuneet siitä käsiä heilutellen tyyliin Haskell laskee vain sen tiedon jota tarvitsee tms. Yleisesti ohjelmointikielen merkitysopissa eli semantiikassa on kaksi puolta: Tarkoitesemantiikka (denotational semantics) käsittelee sitä mitä kielen ilmaukset oikein laskevat ilmaisulla tarkoitettua tulosta. Kurssilla LAP käytettiin tätä lähestymistapaa silloin kun käsiteltiin sitä formaalikieltä, joka koostui niistä syötteistä, joilla automaatti(a vastaava tietokoneohjelma) vastaisi kyllä. Automaateilla tarkoitesemantiikka onkin formaalikielten joukko-oppia. Ohjelmointikielten tarkoitesemantiikassa käytetään sellaisia matemaattisia työvälineitä kuin kategoriateoriaa ja arvoalueteoriaa (joka on luennoijan oma kömpelö käännösehdotelma termille domain theory...). Ohjelmointikielillä tarkoitesemantiikka kuvaileekin niitä funktioita joita ohjelmat esittävät. Yksi osa tarkoitesemantiikkaa on ohjelmointikielen tyyppiteoria (type theory) jolla suljetaan pois sellaisia ilmauksia, jotka kyllä ovat syntaktisesti oikein, muitta joille ei voi antaa järkevää tarkoitettua tulosta. Esimerkiksi ohjelmalla, joka laskisi yhteen totuusarvon ja merkkiijonon, ei ole järkevää tulosta, joten tyypitys sulkeen sen pois. Haskellin tyyppiteoria lähtee siitä, että tyypitys tehdään kokonaan ennen laskentaa, jolloin laskennan aikana ei enää ilmene tyyppivirheitä vaan pelkästään sellaisia suoritusvirheitä kuten vaikkapa yritys jakaa nollalla tms. Emme kuitenkaan uppoudu tällä kurssilla tarkoitesemantiikkaan koska se vaatisi matemaattisen kalustonsa esittelyä ja kehittelyä. Suoritussemantiikka (operational semantics) taas käsittelee sitä miten kielen ilmausten tarkoittamat tulokset lasketaan. Tutustumme siis laiskan laskennan suoritussemantiikan pääpiirteisiin...... samoin kuin sen tavallisemman eli ahkerankin laskennan, jotta näemme missä niiden ero on. Lisäksi tutustumme siihen, miten Haskell-ohjelmoija voi koodissaan ilmoittaa että tämä koodinpätkä olisikin syytä suorittaa ahkerasti ja näin parantaa sen tehokkuutta. 8.1 Lambda-laskennen perusideat Alonzo Churchin 1936 jukaisema lambda- eli λ-laskenta on vakiintunut keskeiseksi formalismiksi ohjelmointikielten teoriassa, joten tutustutaan nyt lyhyesti sen perusideoihin. Church kehitti sen tarkastellakseen sellaista laskentaa, joka etenee sieventämällä monimutkaisempia lausekkeita yksinkertaisemmiksi. 116

Se onkin luonteva lähestymistapa erityisesti funktionaalisissa kielissä, joissa ohjelman suoritusta voidaan ajatella sen yksinkertaistamisena kohden tulostaan. Tilaperustaisissa kielissä oletetaan sievennyksen ohella myös jokin matemaattinen abstraktio muokattavan muistin käsitteelle, jota siis λ-laskennassa ei ole. Erityisesti λ-laskenta on otettu malliksi parametrinvälitykselle ohjelmointikielissä. Määritellään λ-lausekkeet seuraavasti: Muuttujan esiintymä on λ-lauseke. Sellaisenaan se on vapaa (free) esiintymä, koska sitä ei sido (bind) mikään λ. λ-termi λ.e on λ-lauseke, jossa on muuttuja ja e λ-lauseke. Tämä λ sitoo kaikki lausekkeessa e olevat tämän muuttujan vapaat esiintymät. Näin syntyy nykyaikaisista ohjelmointikielistä tuttu muuttujanesittelyiden näkyvyyssääntö: Tämä λ esittelee muuttujan ja siihen sidotut muuttujan esiintymät lausekkeessa e viittaavat tässä esiteltyyn muuttujaan. Jos lausekkeen s sisällä esiintyy jokin toinen (λ.f) niin se esittelee samannimisen mutta eri muuttujan, ja lausekkeen f sisällä muuttujan esiintymät viittaavatkin tähän sisempään esittelyyn. Tällainen λ-termi tarkoittaa ohjelmoijan näkökulmasta sellaista yksiparametrista funktiota, jonka parametrin nimi on tämä ja runko tämä e. Siis sitä, jonka Haskellissa ilmaisemme \ -> e joka onkin yritetty valita muistuttamaan merkkiä λ niin hyvin kuin se on ASCIIlla mahdollista... Kutsutermi muotoa (p q) on λ-lauseke jossa p ja q ovat λ-lausekkeita. Se tarkoittaa tämän funktion p kutsua tällä parametrilla q. Ennen suoritusta tehty tyypintarkastus takaa, että p voidaan sieventää muotoon (ellei se jo ole valmiiksi siinä muodossa) (λ.e). Ideana on että laskenta jatkuu lausekkeella e siten, että tämän vapaat esiintymät on siinä korvattu tällä q. Sovitaan sulkujen vähentämiseksi, että tarkoittaa samaa kuin (p q 1 q 2 q 3... q k ) (...(((p q 1 ) q 2 ) q 3 )... q k ) kuten Haskellissakin eli täälläkin käytämme kuritusta moniparametrisille funktioille, jotka ovat nyt muotoa λ 1.λ 2.λ 3....λ k.f. Muita λ-lausekkeita ei λ-laskennan perusmuodossa ole. 117

Nämä intuitiot parametrien nimien ja funktionkutsujen merkityksestä voidaan lausua seuraavina kahtena periaatteena: α-ekvivalenssi sanoo, että funktiot ovat samat, jossa λ.e ja λy.e[/y] muuttuja y ei esiinny vapaana lausekkeessa e ja e[/y] tarkoittaa sitä λ-lauseketta, joka saadaan korvaamalla jokainen muuttujan vapaa esiintymä lausekkeessa e λ-lausekkeella y. Siis paikallisen muuttujan määrittelyssä voi vaihtaa sen nimeä. Ohjelmointikielissä ajonaikainen järjestelmä huolehtii tästä automaattisesti, se täytyy sanoa ääneen vain λ-laskennan teoriaa esitellessä. β-reduktio taas sanoo, että kutsulauseke (λ.e) f sievenee muotoon e[/f] kunhan ei esiinny vapaana λ-lausekkeessa f mutta senhän voimme aina varmistaa käyttämällä ensin α-ekvivalenssia kutsulausekkeessa. Tämä on se keskeinen sääntö kun λ-laskentaa käytetään ohjelmointikielten ja niillä määritellyn laskennan kuvailussa......koska se ilmaisee mitä funktionkutsun suorittaminen merkitsee. Sen perusintuitio on, että kutsu suoritetaan suorittamalla funktion arvon määrittelevä lauseke e siten, että sen parametrin tilalla onkin sen argumentti f. Eri laskentamallien väliset erot palautuvat oleellisesti siihen, missä järjestyksessä näitä β-reduktioita sovelletaan, kun ohjelman suoritusta ajatellaan tähän tapaan sievennysaskeleina. Kun merkitsemme yhtä tällaista β-reduktioaskelta = β niin voimme esimerkiksi sieventää (λ.λy. y ) (λz.z z) = β λy.(λz.z z) y (λz.z z) = β λy.y y (λz.z z) jossa on alleviivattu se λ johon β-reduktiota nyt sovelletaan. Merkitään vastaavasti = β kokonaista ketjua tällaisia β-reduktioaskeleita. Siis (λ.λy. y ) (λz.z z) = β λy.y y (λz.z z) (14) edellisen kaksiaskelisen ketjun perusteella. Ohjelmointikieliä tarkasteltaessa tätä λ-laskennan perusmuotoa laajennetaan (tarpeen mukaan) esimerkiksi seuraavilla lisäpiirteillä: Perustyypeillä kuten Haskellin Bool, Integer,... Perusoperaatioilla jotka käsittelevät näitä perustyyppisiä arvoja, kuten vaikkapa (+) Integereille. 118

Algebrallisilla tietotyypeillä sekä vastaavilla case-lausekkeilla sentyyppisten arvojen käsittelyyn. Ne voidaan liittää mukaan määrittelemällä niillekin omat β-reduktiosääntönsä. Esimerkiksi Integereiden yhteenlasku (+) voidaan ajatella määritellyn äärettömän monena sääntönä tyyliin 72 + 4543 = β 4615 jne. Nämä perustyyppien perusoperaatiot käsittelevät sentyyppisiä arvoja (value) eli intuitiivisesti loppuun saakka laskettuja välituloksia formaalimmin sellaisia λ-lausekkeita, joihin ei enää voi soveltaa β-reduktioita. Siis esimerkiksi 123 + ((λ.(456 + )) 789) = β 123 + (456 + 789) = β 123 + 1245 = β 1368 koska ulomman yhteenlaskun voi tehdä vain arvoilla, joten sen toinen operandi pitää ensin laskea omaan arvoonsa. 8.2 Normaalimuodoista Normaalimuodon (normal form, NF) käsite tarkoittaa yleisesti lausekkeen tms. sellaista muotoa, johon ei enää voi soveltaa mitään käytettävissä olevista muunnossäännöistä. Nyt λ-laskennan tapauksessa tämä ainoa sovellettava muunnossääntö on β-reduktio, ja siten λ-lausekkeen normaalimuoto tarkoittaa sen arvoa. Tämä normaalimuoto eli arvo voi toki olla funktiotyyppinenkin, kuten yhtälössä (14). Tyypittämätön λ-laskenta sisältää toki termejä joilla ei ole normaalimuotoa onhan päättymättömiä ohjelmiakin! Esimerkiksi jos termiin (λ.( )) (λ.( )) soveltaa β-reduktiota, niin saa vastauksenaan sen itsensä eli siihen voidaan loputtomiin soveltaa β-reduktiota. Mutta jos λ-laskennan termillä on normaalimuoto, niin se on oleellisesti yksikäsitteinen: Jos samalla termillä t on kaksi eri normaalimuotoa u ja v, niin ne ovat keskenään α-ekvivalentit, eli ne eroavat toisistaan vain paikallisten muuttujiensa nimissä. Tästä seuraa että kaikki termistä t alkavat ja loppuun asti lasketut β-reduktioketjut päätyvät oleellisesti samaan tulokseen. Siis tämä normaalimuoto eli arvo eli laskennan lopputulos on hyvin määritelty käsite joka ei riipu laskujärjestyksestä. 119

Tämä ei kuitenkaan tarkoita, että kaikki järjestykset olisivat yhtä hyviä: jotkut järjestykset päättyvät, ja saavuttavat lopputuloksen toiset taas jatkuvat ikuisesti, eivätkä saavuta sitä koskaan. Esimerkiksi λ-lausekkessa (λy.λz.z) ((λ.( )) (λ.( ))) = β λz.z (15) jos sovelletaan β-reduktiota alleviivatussa kohdassa, mutta muuten jäädään samaan ikuiseen silmukkaan kuin edellä. Onko olemassa jokin sellainen periaate valita aina oikea kohta λ-lausekkeessa, että kun juuri siihen sovelletaan β-reduktiota, niin aina lopulta päästään normaalimuotoon, jos sellainen lausekkeella on? Kyllä on, ja jopa yksinkertainen: Kohdista aina β-reduktio ensimmäiseen eli vasemmanpuoleisimpaan mahdolliseen kohtaan lausekkeessa. Tällaista kohtaa, johon voisi soveltaa β-reduktiota, kutsutaan nimellä rede (reducible epression eli redusoituva lauseke). Tämä vuoksi tätä periaatetta valitse aina vasemmanpuoleisin rede kutsutaankin normaaliksi sievennysjärjestykseksi (normal order reduction). Muista kuitenkin seuraava: Tällainen ohjelmointikielen formaali kuvaus ilmoittaa, että sen laskennan pitää näyttää ulkopuolisen tarkkailijan silmissä samalta kuin jos se oikeasti suoritettaisiinkin juuri näin. Kielen toteutus voi sisäisesti toimia miten tahansa, kunhan se säilyttää tämän illuusion. Suorituskelpoinen Haskell-ohjelmakaan ei manipuloi enää lausekkeita, vaan se suorittaakin niistä käännettyä konekoodia, jonka suoritus etenee sitä vastaavasti. Tätä periaatetta pitää tosin täsmentää silloin kun tämä vasemmanpuoleisin lauseke on case valinta of hahmo 1 -> tuloslauseke 1 hahmo 2 -> tuloslauseke 2 hahmo 3 -> tuloslauseke 3. hahmo k -> tuloslauseke k seuraavasti: 1. Valitse samalla periaatteella valinta lausekkeesta kohta, johon sovellat β-reduktiota. 120

2. Jos valinta lauseke muuttui sen tuloksena sellaiseen muotoon, johon jokin näistä hahmo ista sopii, niin korvaa koko tämä case-lauseke ensimmäisen sellaisen hahmon tuloslausekkeella johon olet tehnyt vastaavat nimennät. 3. Muuten palaa takaisin kohtaan 1 sieventämään valinta a edelleen; tai jos se onkin jo normaalimuodossaan, eikä se siltikään sopinut yhteenkään näistä hahmoista, niin lopeta koko laskenta suoritusaikaiseen virheeseen. Nyt voimme lausua ensimmäisen eron ohjelmointikielten erilaisten suoritusmekanismien välillä: Ahkera suoritusjärjestys (call-by-value/cbv, strict, eager) määritellään seuraavasti: Suorita kutsulauseke ((λ.e) f) siten, että 1. ensin sievennät sen parametrilausekkeen f arvoonsa a 2. sitten jatkat sieventämällä lauseketta e[/a]. Se ei siis noudatakaan normaalia sievennysjärjestystä......jolloin se voikin jäädä ikuiseen silmukkaan, kuten yhtälössä (15)... mutta silti useimmat ohjelmointikielet määrittelevät aliohjelmakutsunsa juuri näin... koska silloin ohjelmoija tietää suoritusjärjestyksestä ainakin sen, milloin arvon a laskenta on valmis... joten hän voi käyttää tilaperustaisia piirteitä. Laiska suoritusjärjestys (call-by-need, lazy, non-strict) taas noudattaakin normaalia sievennysjärjestystä. Silloin lauseke e pystyykin valitsemaan itse, mitä osia lausekkeen f arvosta a sen tarvitseekaan oikeasti laskea omaa vastaustaan varten eli silloin tietoriippuvuudet määräävät laskentajärjestyksen. Edellisen mukaan siis laiska suoritusjärjestys pystyy saavuttamaan lopputuloksen aina kun sellainen suinkin on olemassa eli se keksii itse aina oikean sievennystavan...vaikkei siinä ollutkaan paljon keksimistä: Riittää valita aina vasemmanpuoleisin rede. Huomaa kuitenkin, että case-lausekkeissa on kiinteä kokeilujärjestys... jos tämä hahmo sopii mutta mikään sitä edeltävistä ei sopinut. Siten myös laiskassa suoritusjärjestyksessä infinite = case of y -> infinite y [] -> 0 on ikuinen silmukka: sekään ei osaa itse valita jälkimmäistä haaraa, koska edellinenhän sopii aina. Haskell käyttää siis tätä laiskaa suoritusjärjestystä. Normaalilla suoritusjärjestyksellä on myös se etu, että koodia voi manipuloida algebrallisesti miettimättä mitä lisärajoituksia ohjelmointikielen laskentajärjestys aiheuttaa. Toisin sanoen, voi käyttää vapaasti lauseketta, joka määrittelee halutun tuloksen, miettimättä erikseen osaako tämä ohjelmointikieli laskea sen tuloksen kyllä se osaa! 121

Haskell ei myöskään laske vastauksiaan siihen oikeaan normaalimuotoon saakka, jossa kaikki mahdolliset β-reduktiot olisi tehty. Sen sijaan Haskell tyytyykin ns. heikkoon päänormaalimuotoon (weak head normal form, WHNF) jossa lopetetaankin heti, kun normaali sievennysjärjestys ehdottaisi soveltamaan β-reduktiota seuraavaksi sellaiseen kohtaan, joka on... jonkin λ:n sisällä, eli kun lauseke on sieventynyt muotoon λ.((λy.e) f) eli funktioksi, jonka laskenta etenee vasta kun se saa argumentin, koska vasta tämän argumenttinsa saatuaan tämä funktio tietäisi, mitä osia se siitä oikein tarvitseekaan, ja Haskell-laskennanhan määräävät tietoriippuvuudet. jonkin perusoperaation kutsu, mutta sillä on liian vähän argumentteja, joten sitä ei voi suorittaa tätä pidemmälle, kuten vaikkapa sektiota (5 +). jonkin datatyyppinä määritellyn algebrallisen tietotyypin konstruktori, jolla myös on liian vähän argumentteja, joten sitäkään ei voi suorittaa tätä pidemmälle. Muuten jatketaan tekemällä se ehdotettu β-reduktio, jne. Kun sievennettävä Haskell-lauseke h ei olekaan funktiotyyppiä vaan perustyyppiä kuten Integer niin silloin sen WHNF on vastaava arvo. algebrallista tyyppiä niin silloin sen WHNF saadaan sieventämällä sitä kunnes selviää, mikä tämän tyypin konstruktoreista on lausekkeen h alussa. Esimerkiksi jos h :: Maybe Integer niin silloin sen WHNF saadaan sieventämällä, kunnes selviää onko se Nothing vaiko jotakin muotoa Just n. Jos jälkimmäistä, niin sen kentän sisältö n on yhä sieventämättä ellei sisällön n sieventäminen ollut välttämätöntä koko lausekkeen h konstruktorin selvittämiseksi. Vastaavasti case-lausekkeen hahmo t ovatkin muotoa Konstruktori... koska ne kysyvät onko lausekkeen h Konstruktorina tämä? Myös tulkissa ghci käyttäjän kirjoittamaa lauseketta h sievennetään vain sen heikkoon päänormaalimuotoon...... mutta jos tämä h onkin sellaista tyyppiä, joka kuuluukin tyyppiluokkaan Show, niin silloin ghci alkaakin vielä muuntamaan sieventämäänsä tulosta lausekkeelle h vielä merkkijonoksi, jonka se tulostaa käyttäjälle. Tämä muunnos merkkijonoksi taas aiheuttaa sen, että lausekkeen h tulos sievennetäänkin loppuun saakka koska tämän tulostettavan merkkijonon WHNF puolestaan tarvitsee kaikki merkit siitä merkkijonosta, jonka tuottaa kutsu show h. 8.3 Ahkeruuden osoittaminen Haskell-ohjelmassa Haskell tarjoaa primitiivin, jolla ohjelmoija voi ilmoittaa haluavansa, että jokin ohjelman osa suoritetaankin ahkerasti. Kaikkein matalimman abstraktiotason primitiivi on seq :: a -> t -> t 122

jonka tulkinta on sievennä ensimmäinen argumentti tyyppiä a heikkoon päänormaalimuotoonsa w ja palauta tuloksena toinen argumentti tyyppiä b sellaisenaan. Siis Haskell suorittaa ensimmäisen argumenttinsa sivuvaikutuksenaan vaikkei se ole tarpeen tuloksen tekemiseksi. Onneksi Haskell tarjoaa myös tämän primitiivin päälle rakennettuja korkeamman abstraktiotason tapoja ilmaista ahkeruutta. Yksi tällainen tapa ovat Preluden operaattorit ($), ($!) :: (a -> b) -> a -> b f $ = f f $! = seq f joista $ onkin jo tuttu sehän vain kutsuu funktiota f parametrilla normaalisti eli laiskasti $! siis 1. ensin normalisoi parametrin heikkoon päänormaalimuotoonsa w 2. sitten kutsuu funktiota f tällä w. Näillä operaattoreilla on sitten lisätty datatyyppimäärittelyihin mahdollisuus sanoa, että käsittelekin tämä kenttä ahkerasti : Kentän tyypin voi aloittaa huutomerkillä!. Silloin lausekkeessa Konstruktori kenttään 1 kenttään 2 kenttään 3... kenttään k kunkin kentän eteen tulee operaattori ($!) jos sen tyyppi alkaa! ja muuten ($). Silloin esimerkiksi seuraava datatyyppimäärittely on sellain hakupuutyyppi, joka rakennetaankin ahkerasti eikä laiskasti: Funktiossa lisaa konstruktori Solmu normalisoi huutomerkkiensä! vuoksi rekursiokutsunsa lisaa y vasen (ja oikea) tuloksen. Siten se normalisoi jokaisen alipuunsa juuren. Siten rekursio tuottaa puun, jonka jokainen alipuu on kokonaan normalisoitu. data Puu k t = Tyhja Solmu!(Puu k t) k t!(puu k t) deriving (Show) lisaa :: (Ord k) => k -> t -> Puu k t -> Puu k t lisaa y Tyhja = Solmu Tyhja y Tyhja lisaa y haara@(solmu vasen avain tieto oikea) < avain = Solmu (lisaa y vasen) avain tieto oikea > avain = Solmu vasen avain tieto (lisaa y oikea) otherwise = haara 123

Nämä huutomerkit! datatyyppimäärittelyissä ovat jo nyt Haskell-standardissa. Seuraavaan Haskell-standardiin on ehdotettu tällaisten huutomerkkien! sallimista myös hahmoissa (bang patterns) ja ghc toteuttaakin sen yhtenä laajennuksistaan. Silloin voisi kirjoittaa hahmon muotoa!muuttuja tai!_ tarkoittamaan vaivatta, että normalisoi tämä osa heikkoon päänormaalimuotoonsa, vaikka se ei olekaan tarpeen. Haskell-kääntäjän kuten ghc koodin optimoinnin yhtenä työvaiheena on ahkeruusanalyysi (strictness analysis). Se lähtee liikkeelle näistä ohjelmoijan ilmoittamista huutomerkeistä! ja analysoi niiden perusteella, mitkä kaikki muutkin ohjelman osat pitää niiden seurauksena suorittaa ahkerasti Se siis sirottelee ohjelmaan omin päin lisää seq-kutsuja tehostaakseen sitä. Ahkerasta ohjelmakoodista voi nimittäin tuottaa tehokkaampaa konekoodia kuin laiskasta, koska siitä voi jättää pois laiskuuden vaatiman lisäkirjanpidon. Tämä on yksi sellainen optimointivaihe, jota ahkeran kielen kääntäjässä ei ole. Tätä huutomerkkiä! ei kuitenkaan voi käyttää tyyppimäärittelyssä newtype Nimi = Konstruktori Tyyppi vaan tämän uudennimi sen tyypin ahkeruuden tai laiskuuden määrää se olemassa oleva Tyyppi jonka se sisältää. Siten newtype tekee uudennimi sen tyypin joka käyttäytyy laskennassa samoin kuin tämä Tyyppi. Tämä pätee myös silloin kun tällä uudennimisellä tyypillä on tyyppiparametreja. Vastaavanlainen datatyypinmäärittely tekisikin uuden tyypin, joka olisikin laiska, ellei huutomerkillä! muuta ilmaista. Siis sen käyttäytyminen laskennassa määräytyykin datatyyppimäärittelyn perusteella eikä noudattamalla Tyyppiä. Tämä onkin newtype- ja datatyyppimäärittelyjen perustava ero. Tarkastellaan lopuksi esimerkkinä ahkeruuden tehostavasta vaikutuksesta lukulistan yhteenlaskua. Koska (+) on assosiatiivinen, niin voimme valita vapaasti kumpaa foldia käytämme. Valintamme on foldl koska sehän on intuitiivisesti pelkkä silmukka kun taas foldr olisi aitoa rekursiota : foldl :: (a -> b -> a) -> a -> [b] -> a foldl f z [] = z foldl f z (:s) = foldl f (f z ) s foldr :: (a -> b -> b) -> b -> [a] -> b foldr f z [] = z foldr f z (:s) = f (foldr f z s) 124

Lasketaan sitten käsin esimerkkiä normaalissa järjestyksessä: foldl (+) 0 [ 1, 2, 3,..., n ] = βfoldl (+) (0 + 1 ) [ 2, 3,..., n ] = βfoldl (+) ((0 + 1 ) + 2 ) [ 3,..., n ] (16) = βfoldl (+) ((...(((0 + 1 ) + 2 ) + 3 )...) + n ) [] = β((...(((0 + 1 ) + 2 ) + 3 )...) + n ). Eihän tämä laskenta ollutkaan lainkaan tehokasta sehän teki vain vasemmalle vinon kopion oikealle vinosta syötelistastaan! Miksi? Koska näitä välisummia ei tarvittu laskennan kuluessa! 0 + 1 se + 2 ne + 3. Tämä summalauseke sievennetään arvoonsa vasta kun sitä kysytään ja silloin tehdään oleellisesti saman verran työtä kuin foldl-sievennyksissä. Kirjasto Data.List sisältää onneksi myös funktion foldl :: (a -> b -> a) -> a -> [b] -> a foldl f z [] = z foldl f z (:s) = let z = z f in seq z $ foldl f z s joka siis sanoo, että sievennä lisäksi joka askeleessa tämänhetkinen välitulos z heikkoon päänormaalimuotoonsa. Koska nyt tämän välituloksen tyyppi on numero, niin sen sievennys laskee kyseisen välituloksen arvon. Sen laskenta eteneekin siis seuraavasti: jossa foldl (+) 0 [ 1, 2, 3,..., n ] = βfoldl (+) (0 + 1 ) [ 2, 3,..., n ] = β foldl (+) z 1 [ 2, 3,..., n ] = βfoldl (+) (z 1 + 2 ) [ 3,..., n ] = β foldl (+) z 2 [ 3,..., n ] = βfoldl (+) z n [] = βz n z i = 1 + 2 + 3 + + i on tämän välisumman arvo joka on siis nyt yksi luku. Nyt laskentamme etenee kuten haluamme: tehokkaana silmukkana. 125

8.4 Muistin käyttö ja välitulosten jakaminen Edellinen foldl -esimerkki osoitti myös, että laiskan laskennen muistinkäyttökin saattaa tuottaa yllätyksiä: foldl synnyttikin muistiin kokonaisen sieventämättömän summalausekkeen. Tutustutaan siksi lyhyesti myös siihen, miten laiska laskenta käsittelee muistia. Otetaan yksinkertaiseksi esimerkiksemme kahdennusfunktio double :: Int -> Int double = + ja lausekkeen double (double (double 1)) arvon laskenta. Normaalissa järjestyksessä β-reduktio kohdistuu aina vasemmanpuoleisimpaan mahdolliseen paikkaa eli uloimpaan doubleen: double (double (double 1)) = β (double (double 1)) +(double (double 1)). Mutta laskeeko Haskell tämän välituloslausekkeen (double (double 1))... kerran koska se on paikallisen muuttujan arvona vai kahdesti koska tämä esiintyykin kahdesti doublessa? Vain kerran koska tämän esiintymät jakavat saman kopion välituloslausekkeesta muistissa: + double (double 1) Itse asiassa laiskat ohjelmointikielet käyttävätkin toteutuksissaan verkkoreduktiota (graph reduction) joka on λ-laskentaa verkoilla eli niille määriteltyä β-reduktiota, jotta voidaan esittää rakenteiden jako muistissa, vaikka tekstimuotoisille lausekkeille ilman rakenteiden jakoa se onkin alun perin määritelty. Nyt seuraavaa β-reduktiota sovelletaan normaalin järjestyksen mukaan tämän verkon juureen. Siellä on (+) jolla on molemmat operandit, joten aletaan sieventää sitä heikkoon päänormaalimuotoonsa. 126

Mutta sen sievennys tarvitseekin molempien operandiensa arvot, joten ne pitää sieventää ensin omiin päänormaalimuotoihinsa. Sievennetään ensin vasemmanpuoleista operandia. Sieveneekö myös oikea operandi samalla nehän jakavat saman lausekkeen eli aliverkon? Kyllä sievenee, koska vasemman operandin sievennys kirjoittaa tuloksensa siihen samaan muistipaikkaan, jossa lauseke oli ja jonka myös oikea operandi jakaa: + + double 1 Siis vaikka Haskell-kielessä ei olekaan uudelleensijoituslausetta (Monadien ST ja IO ulkopuolella) niin sen ajonaikainen järjestelmä (run-time system, RTS) perustuu uudelleensijoitukseen konekooditasolla. Jatketaan samaan tapaan: + + + 1 127

Nyt alimman solmun (+) molemmat operandit ovat lukuja 1 eli heikossa päänormaalimuodossaan, joten sekin voidaan vihdoin sieventää omaan heikkoon päänormaalimuotoonsa ja jälleen kirjoittaa tämä tulos siihen muistipaikkaan jossa solmu oli: + + 2 1 Nyt voidaan laskea seuraava solmu (+) vastaavasti: + 4 2 1 128

Ja lopuksi vielä juurisolmu (+): 8 4 2 1 Kuviin on katkoviivoilla merkitty se, milloin muistin eri osia ei enää tarvita. Silloin ne palautuvat toteutuksen roskankerääjälle (garbage collector) uudelleenkäytettäviksi. Näin Haskell sieventää jokaisen lausekkeen vain kerran. Jos samaan lausekkeeseen on useita viittauksia, niin kaikki muutkin viittaukset hyötyvät yhden kautta tehdystä sievennystyöstä. Koska double-esimerkissämme oli solmun (+) sekä vasempana että oikeana operandina, ja siten niillä yhteinen lauseke, niin vasemman operandin sievennys tuotti samalla myös oikealle operandille arvon. Jos joutuu tarkastelemaan Haskell-ohjelman ajan- tai tilankäytön yksityiskohtia, niin silloin joutuu miettimään näitä jaettuja esityksiä muistissa. Toisaalta silloin Haskell joutuu käyttämään muistia kirjanpitoon siitä, mitkä rakenteet ovat vielä sieventämättä ja mitkä ovat jo valmiit. Lisäksi jokaiseen muistiviittaukseen liittyy lisätyönä kysymys onko sen kohde tämän kirjanpidon mukaan vielä kesken tai jo valmis? Tämän kirjanpidon sekä monimuotoisuuden vuoksi Haskell ylläpitää tietoalkioitaan muistissa laatikoituina (boed). Tällaisessa laatikossa on tilaa sekä kirjanpidolle että tulokselle. Tämä laatikointi on saman kaltaista kuin Javan kaksi kokonaislukulajia: raaka int joka ei ole olio vs. Integer joka on olio joka on laatikko jossa on int sisällä. Kääntäjälle ghci voi antaa valitsimen unbo-strict-fields jolloin se eliminoi tämän laatikoinnin niistä datatyyppien kentistä, joiden alussa on huutomerkki! niitä ei tarvitse laatikoida, koska nehän lasketaan ahkerasti. 129