Tämä tarina on Fibonaccin lukujen ongelman alkuperäinen muotoilu.



Samankaltaiset tiedostot
Algebralliset tietotyypit ym. TIEA341 Funktio ohjelmointi 1 Syksy 2005

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

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

TIEA341 Funktio-ohjelmointi 1, kevät 2008

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Luku 3. Listankäsittelyä. 3.1 Listat

815338A Ohjelmointikielten periaatteet Harjoitus 6 Vastaukset

Hakupuut. tässä luvussa tarkastelemme puita tiedon tallennusrakenteina

58131 Tietorakenteet ja algoritmit (kevät 2016) Ensimmäinen välikoe, malliratkaisut

Testaa: Vertaa pinon merkkijono syötteeseen merkki kerrallaan. Jos löytyy ero, hylkää. Jos pino tyhjenee samaan aikaan, kun syöte loppuu, niin

Lisää pysähtymisaiheisia ongelmia

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

TIEA341 Funktio-ohjelmointi 1, kevät 2008

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

TIEA341 Funktio-ohjelmointi 1, kevät 2008

TIEA341 Funktio-ohjelmointi 1, kevät 2008

815338A Ohjelmointikielten periaatteet Harjoitus 7 Vastaukset

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

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

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

815338A Ohjelmointikielten periaatteet Harjoitus 2 vastaukset

Geneeriset tyypit. TIES542 Ohjelmointikielten periaatteet, kevät Antti-Juhani Kaijanaho. Jyväskylän yliopisto Tietotekniikan laitos

ITKP102 Ohjelmointi 1 (6 op)

Tietorakenteet ja algoritmit - syksy

811120P Diskreetit rakenteet

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Ohjelmoinnin peruskurssien laaja oppimäärä

A TIETORAKENTEET JA ALGORITMIT

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

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

AVL-puut. eräs tapa tasapainottaa binäärihakupuu siten, että korkeus on O(log n) kun puussa on n avainta

ICS-C2000 Tietojenkäsittelyteoria Kevät 2016

koska sellainen vaaditaan jotta oma tyyppimme Tree k t pääsee jäseneksi Luokkaan Eq.

Algoritmit 2. Luento 2 Ke Timo Männikkö

Taas laskin. TIES341 Funktio ohjelmointi 2 Kevät 2006

Haskell ohjelmointikielen tyyppijärjestelmä

TKT20001 Tietorakenteet ja algoritmit Erilliskoe , malliratkaisut (Jyrki Kivinen)

TIEA341 Funktio-ohjelmointi 1, kevät 2008

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

Säännöllisten kielten sulkeumaominaisuudet

Tietorakenteet, laskuharjoitus 7, ratkaisuja

Tietorakenteet ja algoritmit Johdanto Lauri Malmi / Ari Korhonen

3. Hakupuut. B-puu on hakupuun laji, joka sopii mm. tietokantasovelluksiin, joissa rakenne on talletettu kiintolevylle eikä keskusmuistiin.

Algoritmit 2. Luento 2 To Timo Männikkö

Kuvaus eli funktio f joukolta X joukkoon Y tarkoittaa havainnollisesti vastaavuutta, joka liittää joukon X jokaiseen alkioon joukon Y tietyn alkion.

Tietorakenteet ja algoritmit

Ei-yhteydettömät kielet [Sipser luku 2.3]

Olkoon seuraavaksi G 2 sellainen tasan n solmua sisältävä suunnattu verkko,

Abstraktit tietotyypit. TIEA341 Funktio ohjelmointi 1 Syksy 2005

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

(0 1) 010(0 1) Koska kieli on yksinkertainen, muodostetaan sen tunnistava epädeterministinen q 0 q 1 q 2 q3

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

Algoritmit 1. Luento 3 Ti Timo Männikkö

Algoritmit 1. Luento 1 Ti Timo Männikkö

Pinot, jonot, yleisemmin sekvenssit: kokoelma peräkkäisiä alkioita (lineaarinen järjestys) Yleisempi tilanne: alkioiden hierarkia

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

Algoritmi on periaatteellisella tasolla seuraava:

4. Tehtävässä halutaan todistaa seuraava ongelma ratkeamattomaksi:

Matematiikan tukikurssi, kurssikerta 2

Vaihtoehtoinen tapa määritellä funktioita f : N R on

58131 Tietorakenteet ja algoritmit (kevät 2014) Uusinta- ja erilliskoe, , vastauksia

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

Todistusmenetelmiä Miksi pitää todistaa?

Matematiikan johdantokurssi, syksy 2016 Harjoitus 11, ratkaisuista

Algoritmit 1. Luento 12 Ti Timo Männikkö

Reaalilukuvälit, leikkaus ja unioni (1/2)

ALGORITMIT 1 DEMOVASTAUKSET KEVÄT 2012

Rekursio. Funktio f : N R määritellään yleensä antamalla lauseke funktion arvolle f (n). Vaihtoehtoinen tapa määritellä funktioita f : N R on

5.5 Jäsenninkombinaattoreista

811120P Diskreetit rakenteet

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

Jäsennys. TIEA341 Funktio ohjelmointi 1 Syksy 2005

Tehtävän V.1 ratkaisuehdotus Tietorakenteet, syksy 2003

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

Algoritmit 1. Luento 7 Ti Timo Männikkö

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

Todistus: Aiemmin esitetyn mukaan jos A ja A ovat rekursiivisesti lueteltavia, niin A on rekursiivinen.

811120P Diskreetit rakenteet

Laskennan teoria (kevät 2006) Harjoitus 3, ratkaisuja

JFO: Johdatus funktionaaliseen ohjelmointiin

S BAB ABA A aas bba B bbs c

M = (Q, Σ, Γ, δ, q 0, q acc, q rej )

Tietorakenteet (syksy 2013)

Ohjelmoinnin perusteet Y Python

Jos sekaannuksen vaaraa ei ole, samastamme säännöllisen lausekkeen ja sen esittämän kielen (eli kirjoitamme R vaikka tarkoitammekin L(R)).

Laskennan mallit (syksy 2010) Harjoitus 4, ratkaisuja

Luonnollisen päättelyn luotettavuus

Tietorakenteet ja algoritmit syksy Laskuharjoitus 1

Kerta 2. Kerta 2 Kerta 3 Kerta 4 Kerta Toteuta Pythonilla seuraava ohjelma:

5.3 Ratkeavia ongelmia

AS C-ohjelmoinnin peruskurssi 2013: C-kieli käytännössä ja erot Pythoniin

Matematiikan tukikurssi

Tietueet. Tietueiden määrittely

815338A Ohjelmointikielten periaatteet Harjoitus 4 vastaukset

= 5! 2 2!3! = = 10. Edelleen tästä joukosta voidaan valita kolme särmää yhteensä = 10! 3 3!7! = = 120

T Syksy 2002 Tietojenkäsittelyteorian perusteet Harjoitus 8 Demonstraatiotehtävien ratkaisut

TIEA341 Funktio-ohjelmointi 1, kevät 2008

ELM GROUP 04. Teemu Laakso Henrik Talarmo

Algoritmit 2. Luento 13 Ti Timo Männikkö

14.1 Rekursio tyypitetyssä lambda-kielessä

Transkriptio:

Rekursiosta ja iteraatiosta Oletetaan että meillä on aluksi yksi vastasyntynyt kanipari, joista toinen on uros ja toinen naaras. Kanit saavuttavat sukukypsyyden yhden kuukauden ikäisinä. Kaninaaraan raskaus kestää yhden kuukauden, jonka jälkeen hän synnyttää uuden tällaisen vastasyntyneen parin. Oletetaan, etteivät kanit kuole ja että juuri synnyttänyt naaras tulee heti uudelleen raskaaksi. Montako sukukypsää kaniparia meillä on kunkin kuukauden alussa? Tämä tarina on Fibonaccin lukujen ongelman alkuperäinen muotoilu. Rekursiivinen ratkaisu voidaan kehittää kahtena funktiona nuoret t = vastasyntyneiden kaniparien lukumäärä ajanhetkellä t vanhat t = sukukypsien kaniparien lukumäärä ajanhetkellä t kirjoittamalla auki se, mitä tarina niistä meille kertoo. Sen tuttu muotoilu yhtenä funktiona saadaan huomaamalla, että nuoret on niin yksinkertainen funktio, että se voidaan sijoittaa helposti funktioon vanhat. Iteratiivinen ratkaisu voidaan kehittää laskemalla laiskasti, miten populaatio kehittyy ajan kuluessa: Nykyinen populaatio esitetään parina (vanhat,nuoret). Seuraava populaatio on tarinan mukaan aina (vanhat+nuoret,vanhat). Ensimmäinen populaatio on (0,1). Sen tuttu muotoilu silmukkana saadaan huomaamalla, että emme tarvitsekaan populaation koko historiaa, vaan pelkästään sen nykyisen arvon. fibrek = let nuoret 0 = 1 nuoret t = vanhat (t-1) vanhat 0 = 0 vanhat t = vanhat (t-1) + nuoret (t-1) in vanhat fibit = map fst $ iterate (\ (vanhat,nuoret) -> (vanhat+nuoret,vanhat)) (0,1) Listojen luonti erikoissyntaksilla Haskell tarjoaa listojen luontiin erikoissyntaksin, joka on helppolukuisempi kuin perussyntaksi. Tämän erikoissyntaksin lähtökohta ovat matemaattiset joukkomerkinnät listoihin sovellettuina. 40

Matematiikassa luemme joukkomerkinnän {0, 2, 4,...,100} tarkoittamaan sitä joukkoa, joka saadaan luettelemalla sen alkiot alkaen luvusta 0 jatkuen siitä aina lisäämällä edelliseen lukuun 2, kuten 0, 2, 4 näyttää malliksi päättyen lukuun 100. Sen vastine listana olisi siis takewhile (<= 100) (iterate (+ 2) 0) joka voidaan kirjoittaa erikoissyntaksilla listana [0,2..100] eli sallimme vain yhden (emmekä kahta) mallia siitä, miten luettelo jatkuu. Yleisesti listojen erikoissyntaksi [eka,toka..vika] tarkoittaa siis samaa kuin takewhile (<= vika) (iterate (+ (toka - eka)) eka) Jos toka puuttuu, niin operaationa tuloslistan seuraava alkio on (+ 1) eli seuraava. Esimerkiksi [3..7] = [3,4,5,6,7] Samaa käytetään myös liukulukulistoille, vaikka se ei ole seuraava liukuluku. Jos vika puuttuu, niin koko takewhile-loppuehto jää pois, ja tuloksena on ääretön lista. Esimerkiksi [1,3..] = kaikki parittomat positiiviset kokonaisluvut. Matematiikassa luemme myös mutkikaampia joukkomerkintöjä. Esimerkiksi { a, b,c b N, a {1, 2, 3,...,b 1}, c = } a 2 + b 2 N määrittelee Pythagoraan kolmikot. Eli ne luonnolliset luvut a < b < c joille Pythagoraan lause a 2 + b 2 = c 2 pätee eli kolmio jonka sivujen pituudet ovat a, b ja c on suorakulmainen. Siinä on kolmenlaisia osia: tulos pystyviivan (tai kaksoispisteen : ) vasemmalla puolella muuttujia arvoalueineen oikealla puolella ehtoja muuttujien arvoille oikealla puolella 41

jossa oikealla puolella olevat osat on eroteltu pilkuilla, (tai sanalla ja tai konjunktiomerkillä ). Luemme sen niin, että tulokset on saatu valitsemalla muuttujille niiden arvoalueilta sellaiset arvot, että ehdot ovat voimassa. Luemme merkinnän vasemmalta oikealle: ensin valitaan b, sitten sen pohjalta a ja sitten niiden pohjalta c. Listojen erikoissyntaksissa on vastaava ilmaus [tulos osa, osa, osa,..., osa] jonka jokainen osa on Hahmo <- lauseke jossa lauseke :: [t] ja Hahmo :: t let-lauseke, mutta ilman in-lauseketta ehto :: Bool. Tällaisen Hahmon tai letin oikealla puolella olevat osat sekä tuloslauseke voivat käyttää sen tekemiä nimentöjä. Koko ilmauksen tyyppi on [u] jossa tulos :: u. Ilmauksen deklaratiivinen merkitys on: Hahmo <- lauseke tarkoittaa valitse Hahmoon sopiva alkio siitä listasta, jonka lauseke tuottaa muistisäääntönä <- on ehto tarkoittaa varmista että se on True näin tehdyillä valinnoilla letillä voi tehdä apumääritelmiä tavalliseen tapaan Jokaisesta näin varmistetusta valinnasta tulee yksi tulos ilmauksen tuottamaan listaan. suorakolmiot = [(a,b,c) b <- [2,3..], a <- [1..b-1], let d = aˆ2 + bˆ2 c = isqrt d, d == cˆ2 ] -- Varoitus: Jos argumentin arvo on niin suuri, -- että se ei mahdu Doubleen ilman pyöristystä, -- niin tämä helppo tapa tuottaakin väärän vastauksen! isqrt :: Int -> Int isqrt = floor. (sqrt :: Double -> Double). fromintegral Ilmauksen operationaalinen merkitys on näiden varmistettujen valintojen syvyyssuuntainen etsintä (depth-first search). 42

Siksi ilmauksen osat kannattaa järjestää siten, että aikaisemmat niistä rajoittavat mahdollisimman tehokkaasti myöhäisempiä. Erityisesti korkeintaan yksi Hahmo <- lauseke kannattaa olla sellainen, jonka lauseke tuottaa äärettömän listan, ja se kannattaa sijoittaa heti ensimmäiseksi osaksi. Lisäksi ehto kannattaa sijoittaa heti kun kaikki sen tarvitsemat muuttujat ovat saaneet arvonsa, jotta etsintää ei jatkettaisi turhaan. Haskell muuntaa tämän etsinnän siten, että epäonnistumisten ja peruuttamisen (backtracking) sijasta tuotetaankin lista onnistumisia (turning failure into a list of successes) jossa epäonnistuminen on tyhjä lista [] onnistuminen on yksialkioinen lista [tulos]. Laiska suoritus tekee tästä etsinnästä syvyys- eikä leveyssuuntaista, koska nämä listat lasketaan alkio kerrallaan eikä kokonaan. Tämä käännösaikainen rekursiivinen muunnos on seuraava: [tulos True] on [tulos] [tulos osa] on [tulos osa, True] jossa käsitellään viimeistä osaa. [tulos ehto, osia] on if ehto then [tulos osia] else [] [tulos Hahmo <- lauseke, osia] on let ok Hahmo = [tulos osia] ok _ = [] in concatmap ok lauseke jossa käytetään Preluden funktioita concatmap ok = concat. map ok concat = foldr (++) [] [tulos let määritelmät, osia] on let määritelmät in [tulos osia] Äärelliset, äärettömät ja määrittelemättömät arvot Tähän mennessä olemme nähneet, että listatyypin [t] arvoihin kuuluu sekä äärellisiä että äärettömiä listoja. Miten voisimme kuvailla, mitkä arvot siihen tarkkaan ottaen kuuluvat? Merkitään A τ = tyypin τ kaikkien mahdollisten arvojen joukko. Listojen määritelmän mukaan e A [t] e on tyhjä lista [] tai muotoa x:xs jossa x A t ja xs A [t]. (10) Millaisia ovat sen mukaiset joukot A [t]? 43

Voimme lukea määritelmän (10) erikseen kumpaankin suuntaan: : Sääntönä jos e on tyhjä lista [] tai muotoa x:xs jossa x A t ja xs A [t], niin myöskin e A [t]. : Joukon A [t] pitää olla suljettu (closed) tämän säännön suhteen, eli A [t] ei enää saa muuttua, vaikka sääntöä sovellettaisiin uudelleen. Matemaattisesti puhutaan tämän säännön kiintopisteen muodostamisesta eli sellaisen joukon muodostamisesta, jota sääntö ei enää muuta. Matematiikassa ns. Knasterin ja Tarskin kiintopistelause takaa, että tällaiset tietojenkäsittelyteorian tarvitsemat kiintopisteet ovat olemassa. Tällä määritelmän (10) lukutavalla saadaan sen pienin (least) kiintopiste: Laskentasääntönä: A pienin [t] = vain ne alkiot, jotka täytyy ottaa mukaan 1 Aloitetaan tyhjästä joukosta A pienin [t] =. 2 Tyhjä lista [] täytyy ottaa mukaan joukkoon A pienin [t]. 3 Jos x A t ja xs on jo otettu mukaan joukkoon A pienin [t], niin myöskin x:xs täytyy ottaa mukaan joukkoon A pienin [t]. 4 Toista askelta 3 (ikuisesti...) kunnes joukko A pienin [t] ei enää kasva. Voimme osoittaa listainduktiolla että jokin väite φ pätee kaikille tämän joukon A pienin [t] alkioille: Perustapaus on osoittaa, että φ pätee tyhjälle listalle []. Induktiivinen tapaus ❶ olettaa, että φ pätee listalle xs A pienin [t] ❷ osoittaa induktio-oletuksen ❶ perusteella, että φ pätee myös jokaiselle listalle x:xs A pienin [t] jossa x A t on mielivaltainen. Silloin φ pätee kaikille joukon A pienin [t] alkioille, koska askeleen 4 mukaan käsittelimme kaikki eri tavat joilla siihen on voitu lisätä alkioita. Vertaa tätä listainduktiota tuttuun luonnollisten lukujen N induktioon: Siinä N on kuin A pienin [()]. Vertaa tätä listainduktiota myös foldr-rekursioon. Osoitetaan listainduktiolla, että poly1 [c 0, c 1, c 2,...,c n ] x = n c i x i. (11) i=0 Toisin sanoen, jos listana on polynomin p kertoimet pienimmästä suurimpaan, niin lasketaan sen arvo p(x) annetulla x. Näin laskemalla vältetään potenssiinkorotukset x i. 44

Perustapaus: poly1 [ ] x = 0 joka on oikein, koska vastaavassa polynomissa p ei ole yhtään termiä, ja sellainen tyhjä summa = yhteenlaskun neutraalialkio. Induktio-oletus: poly1 [c 1, c 2,...,c n ] x = n c i x i 1. Induktiivinen tapaus alkaa koodin algbrallisella käsittelyllä: Siten i=1 poly1 (c:cs) x = foldr (\ c y -> c + x * y) 0 (c:cs) x = c + x * (foldr (\ c y -> c + x * y) 0 cs) = c + x * poly1 cs x poly1 [c 0, c 1, c 2,...,c n ] x =c 0 + x (poly1 [c 1, c 2,...,c n ] x) } {{ } induktio-oletus sopii tähän n =c 0 + x c i x i 1 i=1 joka on haluttu väite. poly1,poly2 :: [Double] -> Double -> Double poly1 cs x = foldr (\ c y -> c + x * y) 0 cs poly2 = foldr (\ c f -> \ x -> c + x * f x) (const 0) Tämä joukko A pienin [t] koostuu täsmälleen kaikista äärellisistä listoista, joiden jokainen alkio on tyyppiä t. Se kuvaa ahkeran ohjelmointikielen tyypin [t]. Laiskan Haskellin tyypissä [t] on kuitenkin myös ne äärettömät listat, joiden jokainen alkio on tyyppiä t. Muodostetaan siis suurin (greatest) kiintopiste A suurin [t] = kaikki ne alkiot, joita ei ole pakko jättää pois lukemalla määritelmä (10) päinvastoin: 45

[] alku : [t] A t Kuva 2: Lista-automaatti. : Saamme kontraposition kautta säännön jos e ei ole tyhjä lista [] tai e ei ole muotoa x:xs tai e on sitä muotoa mutta x A t tai xs A suurin [t], niin myöskään e A suurin [t]. : Joukon A suurin [t] pitää olla suljettu tämän alkioiden poistosäännön suhteen. Lopulta tämä joukko A suurin [t] koostuu täsmälleen niistä äärellisistä ja äärettömistä listoista, joiden jokainen alkio ovat tyyppiä t koska täsmälleen niitä ei ollut pakko jättää pois tämän poistosäännön nojalla. Kuvan 2 lista-automaatti esittää määritelmän (10). (Jätetään toistaiseksi huomiotta sen katkoviivoitettu siirtymä, johon palataan hieman myöhemmin.) A pienin [t] A suurin [t] = ne syötteet, jotka se hyväksyy. = ne (myös äärettömät) syötteet, joita se ei hylkää. Syöte hylätään, jos sen lopussa ei olla hyväksyvässä tilassa tai jos nykyisessä tilassa ei ole siirtymää sen seuraavalla syötemerkillä. Siis ääretöntä syötettä ei hylätä täsmälleen silloin jos siirtymiä voi seurata ikuisesti. Tätä joukon A suurin [t] konstruktiota alkioita pois jättämällä kutsutaan koinduktiiviseksi. Tässä joukossa A suurin [t] induktio ei enää pädekään, koska siellä on muutakin kuin vain induktiivisesti muodostettu A pienin [t] siellä on myös ne (oikein muodostetut) äarettömät listat. Esimerkiksi listainduktiolla osoittamamme tulos (11) ei enää pädekään, jos poly1 saakin syötteekseen äärettömän listan [c 0, c 1, c 2,...]: Silloin poly1 jumiutuu päättymättömään rekursioon, vaikka esimerkiksi arvolla x = 0 oikea vastaus olisikin listan ensimmäinen alkio c 0. Yksi laiskan laskennan epämiellyttävistä yllätyksistä siis on, ettei induktio pädekään. Onneksi usein riittää osoittaa Haskell-ohjelman toiminta äärellisillä syötteillä, ja silloin induktio yhä pätee. Tai voi käsitellä sen toiminnan erikseen äärellisillä (käyttäen induktiota) ja äärettömillä syötteillä. Tai voi käyttää induktiivisen sijasta aitoa koinduktiivista päättelyä, joka kuitenkin sivuutetaan tällä kurssilla. 46

Yksinkertaista koinduktiivista päättelyä äärettömillä listoilla voi tehdä käyttämällä kuvan 2 lista-automaattia testisyötegeneraattorina. Esimerkiksi voimme laskea poly1 alku x = foldr (\ c y -> c + x * y) 0 alku = foldr (\ c y -> c + x * y) 0 (a:alku) = a + x * (foldr (\ c y -> c + x * y) 0 alku) = a + x * (poly1 alku x) josta havaitsemme, että ikuinen laskenta toistuu ennen kuin tulosta syntyy. Toisaalta laskemalla map g alku = foldr ((:). g) [] alku = foldr ((:). g) [] (a:alku) = (g a) : foldr ((:). g) [] alku = (g a) : map g alku havaitsemme, että map pystyy tuottamaan tuloslistaansa laisksti alkio kerrallaan myös silloinkin, kun sen syötelista on ääretön. Edes tämä koinduktiivinen konstruktio A suurin [t] ei vieläkään kuvaa tarkasti Haskelllistatyypin [t] koko arvojoukkoa A [t] : Laiskan suorituksen vuoksi myös määritelmä ikuinen :: [t] ikuinen = ikuinen hyväksytään ja pysähtyy. Niinpä sen täytyy määritellä tälle muuttujalle jokin arvo. Tätä arvoa kutsutaan tavallisesti nimellä [t] eli tyypin [t] pohja-arvo (bottom). Ahkerassa ohjelmointikielessä tilanne olisi yksinkertaisempi: Yritys tehdä tällainen ikuinen määritelmä johtaisi heti ohjelman jumiutumiseen eli lopputulokseen u joten siellä minkään muuttujan arvo ei voisi olla u. Laiskassa ohjelmointikielessä muuttujan arvoa ei lasketakaan valmiiksi sen määrittelyhetkellä. Sen sijaan muuttujalle annettu arvo on (enemmän tai vähemmän keskeneräinen) laskenta joka sieventyy vaiheittain kohti lopullista arvoaan sitä mukaa kun ohjelman suoritus tarvitsee muuttujan esittämää informaatiota. Sellaisen muuttujan arvo voi siten olla myös sellainen laskenta, joka aikanaan jumiutuu......eli sellainen tietorakenne, jossa aikanaan kohdataan osa u. Listoille esitimme tämän mahdollisuuden katkoviivoitettuna siirtymänä kuvan 2 automaatissa. 47

Tämä pätee jo perustyypeissäkin, joten esimerkiksi ja niin edelleen. A Bool = {True,False, Bool } Kun kysytään funktion tulosta, niin sen laskenta voi jumittua, jolloin u on sen luonteva tulos. Mutta onko u luonteva tulos silloinkin, kun vain kysytään jonkin (perustyypin...) muuttujan arvoa? Tämäkin on siis epämiellyttävä yllätys laiskassa laskennassa. Sama u esittää laskentaa, jonka tuloksen tyyppi on u, mutta joka ei pystykään tuottamaan sitä, koska se pyöriikin ikuisessa rekursiossa, tai pysähtyykin ajonaikaiseen virheeseen esimerkiksi siihen, ettei mikään caselausekkeen Hahmoista sopinutkaan. Haskellissa ei voi testata olisiko jonkin lausekkeen arvo u vaiko ei, koska sehän olisi ratkeamatonta Lauseen 4 nojalla. Pienimpien kiintopisteiden teoriaa käytetään esimerkiksi ahkerien (eli lähes kaikkien) ohjelmointikielten teoriassa kuten rekursion ja toistorakenteiden semantiikassa osoitettaessa (esimerkiksi induktiolla) että ohjelman laskema lopputulos on haluttu. Suurimpien kiintopisteiden teoriaa käytetään puolestaan esimerkiksi osoitettaessa sellaisen ohjelman oikeellisuutta, joka ei pysähdy ja anna lopputulosta, vaan jonka on tarkoitus toimia ikuisesti. Sellaisia ovat esimerkiksi tietoliikenneprotokollat, verkko- ja tietokantapalvelimet,... Voidaan tarkastella sellaisen ohjelman periaatteessa äärettömiä mahdollisia toimintahistorioita ja osoittaa, että niillä on halutut ominaisuudet, kuten vaikkapa että jokaiseen saatuun viestiin vastataan. 4.4.4 Tulkkaamisesta ja kääntämisestä poly2 on sama funktio kuin poly1 mutta niissä laskenta etenee niissä eri tavalla. Tämä samuus tarkoittaa että jokaisella ds ja z pätee poly1 ds z = poly2 ds z jonka voi osoittaa induktiolla listan ds suhteen. poly1 cs x tuottaa foldr-rekursiolla polynomin p arvon tyyppiä Double annetulla x. Siten poly1 cs on funktio, joka odottaa arvoa x, ja sen saatuaan etenee foldrrekursiolla listan cs suhteen. 48

poly2 cs tuottaa foldr-rekursiolla nimettömän funktion tyyppiä Double -> Double joka laskee polynomin p arvon kun sille annetaan x. Siten poly2 cs muuntaakin listan cs foldr-rekursiolla funktioksi, joka laskee polynomin p arvoja kun sille annetaan arvoja x. Intuitiivisesti poly1 tulkkaa tietorakenteena esitettyä ohjelmaa cs nykyisellä syötteellä x poly2 ensin kääntää ohjelman cs suorituskelpoiseksi koodiksi jota voi sitten suorittaa eri syötteillä x ja funktionaalisessa ohjelmoinnissa voi ilmaista tämän vaihejaon. 4.4.5 Tyyppisynonyymit Haskellissa voi määritellä lyhennenimen tyypille. Tämän määritelmän syntaksi on type Lyhenne = PitkäTyyppi Tämän määritelmän nojalla Lyhenne tarkoittaa samaa kuin PitkäTyyppi ja sitä voi käyttää samalla tavalla. Tällainen type-määritelmä on tehtävä lähdekooditiedoston uloimmalla tasolla eli ne eivät saa olla minkään letin tai wheren sisällä. Esimerkiksi vakiokirjasto Prelude määrittelee lyhenteen type String = [Char] koska Haskellissa ei ole erillistä merkkijonotyyppiä, vaan merkkijonot ovat merkkilistoja. Tyyppiparametrit type-määritelmässä voi olla myös yksi tai useampi parametri ennen yhtäsuuruusmerkkiä = samoin kuin Haskell-funktioissa: type Lyhenne par_1 par_2 par_3... par_k = PitkäTyyppi Jokainen parametri on oma muuttuja. Tällainen parametri tarkoittaa vielä tuntematonta Tyyppiä. PitkäTyyppi voi käyttää näitä parametreja perustyyppien tavoin. Tällaisen parametrisoidun määritelmän käyttö Tyyppinä on vastaavasti funktionkutsun kaltainen: Lyhenne Tyyppi_1 Tyyppi_2 Tyyppi_3... Tyyppi_k Se tarkoittaa sellaista PitkäTyyppiä, jossa jokaisen par_i korvasi sitä vastaava Tyyppi_i. Esimerkiksi 49

type Nimetty t = [(String,t)] määrittelee lyhenteen Nimetty t tyypille lista, jonka alkiotyyppi on pari, jonka ensimmäinen osa on tyyppiä String ja toinen osa on tyyppiä t. Siten käyttö Nimetty Int on Tyyppinä [(String,Int)] käyttö Nimetty Bool on Tyyppinä [(String,Bool)] käyttö Nimetty (Nimetty Double) on Tyyppinä[(String,Nimetty Double)] eli [(String,[(String,Double)])] ja niin edelleen. 4.4.6 Uusien tyyppien määrittely Haskellissa voi määritellä myös kokonaan uusia tyyppejä. Tämän määritelmän syntaksi on data Uusi = Konstruktori_1 kentät_1 Konstruktori_2 kentät_2 Konstruktori_3 kentät_3... Konstruktori_p kentät_p deriving (Show) Jokainen Konstruktori_i on kokonaan uusi Nimi jonka määritelmä varaa vain tähän tarkoitukseen eli samaa Konstruktoria ei saa määritellä uudelleen tässä samassa eikä missään muussakaan data-määritelmässä. Sen kentät_i ovat muotoa Tyyppi_i1 Tyyppi_i2 Tyyppi_i3... Tyyppi_iq deriving-osa ei ole välttämätön, mutta ilman sitä Haskell määrittelee sellaisen tyypin, jonka arvoja ei voi tulostaa käyttäjälle. Tällainen data-määritelmä on tehtävä lähdekooditiedoston uloimmalla tasolla. Tämä data-määritelmä luo kokonaan uuden tyypin jonka arvojoukko A Uusi koostuu alkioista Konstruktori_i arvo_i1 arvo_i2 arvo_i3... arvo_iq jossa jokainen arvo_ij :: Tyyppi_ij on vastaavan kentän sisältämä arvo eli sen oman arvojoukon A Tyyppi ij jokin alkio. Koska Haskell on laiska ohjelmointikieli, niin tämä arvojoukko A Uusi on koinduktiivinen A suurin Uusi sisältää pohja-arvon Uusi. Esimerkiksi (valmiiksi määritellyn) totuusarvotyypin määritelmä olisi voinut olla 50

data Bool = False True eli sillä on 2 Konstruktoria joilta kummaltakin puuttuvat kaikki kentät. Myös nämä data-määritelmät voivat sisältää samanlaisia tyyppiparametreja kuin type-määritelmätkin. Esimerkiksi vakiokirjasto Prelude sisältää määritelmän data Maybe t = Nothing Just t eli tyyppi, jonka arvot ovat ovat joko Justiinsa tyypin t arvo tai ei mitään joten esimerkiksi A suurin Maybe Bool = { Maybe Bool,Nothing, Prelude sisältää myös määritelmän data Either l r = Left l Right r Just False,Just True,Just Bool }. Lajit Haskellissa on tyyppien tyypit eli lajit (kind): Siten * on tyyppien laji. a -> b on sellaisen 1-parametrisen tyyppikonstruktorin laji, jonka parametrin laji on a ja tuloksen laji on b. a -> b -> c on 2-parametrisen tyyppikonstrukorin laji. Bool :: * Maybe :: * -> * Either :: * -> * -> * Maybe Bool :: * Either (Maybe Bool) :: * -> * ja niin edelleen. Pääasiallisesta tyypistä Tälläinen tyyppiparametrien tarjoama tietorakenteiden ja niiden käsittelyfunktioiden monikäyttöisyys on parametrista monimuotoisuutta (parametric polymorphism). Monimuotoisuutta käsitellään kurssilla lisää myöhemmin. Näiden tyyppiparametrien vuoksi täsmennämme aiempaa täsmällisen tyypityksen periaatettamme: 51

Jokaisella lausekkeella on oleellisesti täsmälleen yksi pääasiallinen (principal) tyyppi, josta kaikki sen muut monimuotoiset tyypit saadaan korvaamalla tyypeillä sen tyyppiparametrit. Tässä oleellisesti tarkoittaa, että tyyppiparametreille valitut nimet eivät ole merkityksellisiä (vertaa α-konversio). Siten[t] -> t -> t ja[u] -> u -> u ovat oleellisesti yksi ja sama tyyppi, ja niin edelleen. Esimerkiksi pääasiallinen tyyppi foldr :: (a -> b -> b) -> b -> [a] -> b tarkoittaa, että foldr on myös jokaista tyyppiä, joka saadaan siitä korvaamalla jokainen a keskenään samalla tyypillä t ja jokainen b keskenään samalla tyypillä u. Esimerkiksi... a on b on foldr on Int Bool (Int->Bool->Bool)->Bool->[Int]->Bool Bool Int (Bool->Int->Int)->Int->[Bool]->Int Char Int (Char->Int->Int)->Int->[Char]->Int Int [Int] (Int->[Int]->[Int])->[Int]->[Int]->[Int]... Haskell päättelee tämän pääasiallisen eli yleisimmän tyypin eli rajoittaa tyypinpäättelynsä aikana tyyppejä vain sen verran kuin on pakko. Rekursiiviset tyypit data-määritelmä voi viitata itseensä eli sen Uusi tyyppi(konstruktori) voi esiintyä myös sen omissa kentissä (myös välillisesti muiden Tyyppien kautta). Esimerkiksi (valmiiksi määritellyn) listatyyppikonstruktorin määritelmä olisi voinut olla data [t] = [] t : [t] jossa arvokonstruktorioperaattorin (:) oikeanpuoleisen kentän Tyyppi on tämä sama määriteltävänä oleva [t] itse. Esimerkkinä määritellään binäärinen hakupuu (ilman tasapainottamista). Oppikirjamääritelmän mukaan binäärinen hakupuu on joko tyhjä tai solmu jolla on 4 kenttää: 1 vasen alipuu 2 avain 3 avainta vastaava tietoalkio, ja 4 oikea alipuu. Nämä vasen 1 ja oikea 4 alipuu ovat induktiivisesti myöskin (pienempiä) hakupuita. Sovitaan avainten 2 tyypiksi Int. 52

Jätetään tietoalkioiden 3 tyypiksi tyyppiparametri t jotta samalla määritelmällä saadaan hakupuutyypit eri tietoalkiotyypeille. Kirjoitetaan tämä määritelmä Haskelliksi. data Tree t = Empty Node (Tree t) Int t (Tree t) deriving (Show) additem :: Int -> t -> Tree t -> Tree t additem newkey newitem Empty = Node Empty newkey newitem Empty additem newkey newitem (Node left oldkey olditem right) newkey < oldkey = Node (additem newkey newitem left) oldkey olditem right newkey > oldkey = Node left oldkey olditem (additem newkey newitem right) otherwise = Node left oldkey newitem right build :: [(Int,t)] -> Tree t build = foldr (uncurry additem) Empty itemof :: Int -> Tree t -> Maybe t itemof _ Empty = Nothing itemof key (Node left oldkey olditem right) key < oldkey = itemof key left key > oldkey = itemof key right otherwise = Just olditem contentsof :: Tree t -> [(Int,t)] contentsof Empty = [] contentsof (Node left key item right) = contentsof left ++ (key,item) : contentsof right contentsof :: Tree t -> [(Int,t)] contentsof = let cont Empty acc = acc cont (Node left key item right) acc = cont left $ (key,item) : cont right acc in flip cont [] Tällaisen rekursiivisen tyypin arvojoukko A suurin Uusi on korekursiivinen kuten listojen A suurin [t] mutta monimutkaisempi, koska sillä voi olla useita konstruktoreita ja niillä monia rekursiivisia kenttiä. 53

Esimerkiksi rusetti = Node rusetti 1 "i" rusetti on määritelmän mukainen hakupuu jonka Tyyppi on Tree String. Tyyppi Maybe t on puolestaan luonteva hakuoperaation itemof tulokselle koska se on Just se jos puussa on annettua avainta key vastaava tietoalkio Nothing jos sellaista ei ole. Siten Maybe on tyyppiturvallinen tapa ilmaista sellaiset funktiot, joilla ei ehkä olekaan mitään oikeaa tulosta. Rakenteinen induktio ja rekursio Aiemmin näimme myös listainduktion, jolla voi osoittaa väitteitä listatyypin arvoista. Se on erikoistapaus rakenteisesta (structural) induktiosta, jolla voi osoittaa väitteitä rekursiivisesti määriteltyjen Tyyppien arvoista. Koska laiskassa kielessä rekursiivisten tyyppien arvojoukot kuten A suurin Uusi sisältävät myös äärettömiä alkioita kuten rusetti ja pohja-arvon kuten Uusi ei induktio taaskaan tarkkaan ottaen päde......mutta se pätee yhä äärellisillä arvoilla A pienin Uusi Asuurin Uusi. Kun halutaan osoittaa väite φ rakenteisella induktiolla jonkin tyypin u kaikille äärellisille arvoille A pienin u niin... perustapauksena tarkastellaan jokaista tyypin u sellaista konstruktoria, jolla ei ole yhtään rekursiivista kenttää, ja osoitetaan että φ pätee sille. Hakupuutyypissämme Tree t osoitettaisiin φ siis sen konstruktorille Empty. induktiivisena tapauksena tarkastellaan jokaista tyypin u sellaista konstruktoria, jolla on rekursiivisia kenttiä, ja osoitetaan että φ pätee sille, kun oletetaan että φ pätee niiden kenttien arvoille. Hakupuutyypissämme Tree t osoitettaisiin φ siis sen konstruktorille Node kun oletetaan, että φ pätee vasemmalle ja oikealle alipuulle left ja right. Jos tyypin u jokaisella konstruktorilla on rekursiivisia kenttiä, niin tyypillä u on vain äärettömiä arvoja (sekä u ) eikä induktio edes lähde käyntiin tämäkin on siis mahdollista laiskassa kielessä. Osoitetaan rakenteisella induktiolla, että funktio contentsof palauttaa saamansa puun (avain,sen tietoalkio)-parit sisäjärjestyksessä. Perustapauksena tyhjä puu Empty palauttaa tyhjän listan [] ja se on oikein, koska tyhjässä puussa ei ole solmuja. Induktiivisena tapauksena tarkastellaan solmua Node left key item right. Induktio-oletuksena on, että contentsof left palautti sen vasemman alipuun ja contentsof right sen oikean alipuun parit sisäjärjestyksessä. 54

Sen nojalla myös tämä solmu palauttaa oikean tuloksen, koska vastaavassa rekursiohaarassa sen(key,item)-pari sijoitetaan niiden väliin, eli oikealle paikalleen. Tämä contentsof on funktionaalinen vastine iteraattorille: Sen avulla voimme kulkea puun solmu solmulta (tässä sisäjärjestyksessä) laiskana listana, ilman erityistä iteraattoria (joka olisikin tilallinen idea). Voimme myös ohjelmoida rekursiivisesti määritellyn tietotyypin rakenteen suhteen listarekursion tapaan: Määritellään funktio tapauksina siten, että... perustapauksina käsitellään ne konstruktorit, joissa ei ole rekursiivisia kenttiä. Niissä tulos voidaan määritellä suoraan ilman rekursiokutsuja. rekursiivisina tapauksina käsitellään ne konstruktorit, joissa on rekursiivisia kenttiä. Nämä rekursiiviset kentät käsitellään sopivilla rekursiokutsuilla. Kehitämme esimerkkinä funktion additem, joka lisää annetun avaimen newkey ja sen tietoalkion newitem hakupuuhun. Viittausten läpikuultavuuden vuoksi lisääminen tarkoittaa sellaisen uuden puun rakentamista, jossa on myös tämä uusi (newkey,newitem)-pari. Myös vanha puu säilyy muuttumattomana. Jakaudutaan ensin tapauksiin puutyypin konstruktorien mukaan: additem :: Int -> t -> Tree t -> Tree t additem newkey newitem Empty =? additem newkey newitem (Node left oldkey olditem right) =? Ei-rekursiivisen konstruktorin Empty ei-rekursiivinen haara on 1-solmuinen puu, jossa on vain tämä pari: additem newkey newitem Empty = Node Empty newkey newitem Empty Rekursiivisen konstruktorin Node rekursiivisessa haarassa sen Hahmo antoi nykyisen solmun avaimelle nimen oldkey joten voimme haarautua edelleen veraamalla sitä uuteen avaimeen newkey: additem newkey newitem (Node left oldkey olditem right) = newkey < oldkey =? newkey > oldkey =? otherwise =? Tässä käytetään Preluden määrittelemää vakiota otherwise = True esittämään tällaista viimeistä vartijaa. Tässä otherwise-haarassa oldkey ja newkey ovat siis samat, joten siinä vain korvataan oldkeyn vanha tietoalkio olditem uudella tietoalkiolla newitem: otherwise = Node left oldkey newitem right 55

Kun newkey < oldkey niin lisäys tehdään vasempaan alipuuhun, jolle Hahmo antoi nimen left. Koska kyseessä on rekursiivinen kenttä, käytetään rekursiivista kutsua: newkey < oldkey = Node (additem newkey newitem left) oldkey olditem right Ja symmetrisesti lisäys oikeaan alipuuhun nimeltään right: newkey > oldkey = Node left oldkey olditem (additem newkey newitem right) Nyt funktiolla on kaikki haarat eli se on valmis. Jokaisella rekursiivisesti määritellyllä tietotyypillä onkin oma luontainen rekursionsa joka voitaisiin määritellä samaan tapaan kuin foldr listoille. Nämäkin foldit ovat monikäyttöisiä, joskaan eivät aivan yhtä yleisesti käytettyjä kuin listoilla. Samaa luontaisen käsittelytavan ideaa voi käyttää muissakin kuin rekursiivisissa tietotyypeissä. Prelude sisältää esimerkiksi funktion maybe :: b -> (a -> b) -> Maybe a -> b joka esittää luontaisen tavan jolla Tyyppiä Maybe a olevia arvoja käsitellään silloin kun tuloksena halutaan Tyyppiä b oleva arvo: Nothingin vastine on jokin arvo Just x vie arvon x :: a funktioon joka antaa tuloksen Tyyppiä b. Kerääjäparametreista ja jatkeista contentsof on siis hyödyllinen funktio mutta tehoton: Kun contentsof käsittelee nykyistä Nodea, niin se käy läpi koko vasemman alipuunsa left tuottaman listan voidakseen liittää sen perään nykyisen Noden pari (key,item) sekä oikean alipuun right tuottaman listan. Siten contentsof käy samoja pareja läpi yhä uudelleen. Esimerkiksi vasemmalle nojaavassa n-solmuisessa puussa build [(i,()) i <- [1..n]] jokaisen solmun (i,()) lisääminen käy uudelleen läpi koko vasemmassa alipuussa luodun listan ja niin edelleen. [(1,()),(2,()),(3,()),...,(i 1,())] Tämän moninkertaisen läpikäynnin takia contentsof vie kokonaisuudessaan pahimmillaan Ω(n 2 ) askelta. Jotta solmulista olisi riittävän tehokas funktionaalinen vastine iteraattoreille, niin se täytyy pystyä muodostamaan O(n) askeleessa. 56

Tämän hitauden syynä on siis, että listan loppuun lisääminen on hidasta mutta alkuun nopeaa. Kehitetään siis funktiosta contentsof versio, jossa pari (key,item) lisätäänkin sopivan listan alkuun. Lähdetään siis kehittämään uutta funktiota cont :: Tree t -> [(Int,t)] -> [(Int,t)] cont puu acc = contentsof puu ++ acc eli jossa lisäparametri acc on se lista jolla vanhan funktion contentsof tulosta pitää jatkaa ja jonka alkuun pari (key,item) voidaan nopeasti liittää. Tässä määritellään se tulos, jonka uusi funktio cont laskee mutta tavoitteenamme on sellainen funktio, joka laskee sen eri tavalla ja nopeammin kuin tämä määritelmä sen tekisi. Lisäparametrin acc alkuarvon voi laskea tuloksen määritelmästä cont puu alkuarvo = contentsof puu ++ alkuarvo ja siitä, että tällä alkuarvolla haluamme lopputulokseksi = contentsof puu josta saadaan alkuarvoksi listojen konkatenaatio-operaattorin (++) (oikeanpuoleinen) neutraalialkio []. Empty-haaran voi laskea cont Empty acc = contentsof Empty ++ acc soveltamalla tuloksen määritelmää vasemmalta oikealle = [] ++ acc funktion contentsof Empty-haarasta = acc Node-haaran voi laskea cont (Node left key item right) acc = contentsof (Node left key item right) ++ acc soveltamalla tuloksen määritelmää vasemmalta oikealle = (contentsof left ++ (key,item) : contentsof right) ++ acc funktion contentsof Node-haarasta. Siihen sovelletaan listojen konkatenaatiooperaattorin (++) liitännäisyyttä (associativity) (x ++ y) ++ z = x ++ (y ++ z) = contentsof left ++ ((key,item) : contentsof right ++ acc) = cont left ((key,item) : cont right acc) soveltamalla kahdesti tuloksen määritelmää oikealta vasemmalle. contentsof yhdistää nämä kolme laskelmaa. 57