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



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

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


Toinen muotoilu. {A 1,A 2,...,A n,b } 0, Edellinen sääntö toisin: Lause 2.5.{A 1,A 2,...,A n } B täsmälleen silloin kun 1 / 13

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

Luku 3. Listankäsittelyä. 3.1 Listat

TIEA341 Funktio-ohjelmointi 1, kevät 2008

-Matematiikka on aksiomaattinen järjestelmä. -uusi tieto voidaan perustella edellisten tietojen avulla, tätä kutsutaan todistamiseksi

Yhtälönratkaisusta. Johanna Rämö, Helsingin yliopisto. 22. syyskuuta 2014

TIEA341 Funktio-ohjelmointi 1, kevät 2008

815338A Ohjelmointikielten periaatteet Harjoitus 6 Vastaukset

Ydin-Haskell Tiivismoniste

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

TIEA241 Automaatit ja kieliopit, syksy Antti-Juhani Kaijanaho. 8. syyskuuta 2016

1. Osoita, että joukon X osajoukoille A ja B on voimassa toinen ns. de Morganin laki (A B) = A B.

815338A Ohjelmointikielten periaatteet Harjoitus 7 Vastaukset

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Tietotekniikan valintakoe

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

Approbatur 3, demo 1, ratkaisut A sanoo: Vähintään yksi meistä on retku. Tehtävänä on päätellä, mitä tyyppiä A ja B ovat.

Tehtävä 1. Päättele resoluutiolla seuraavista klausuulijoukoista. a. 1 {p 3 } oletus. 4 {p 1, p 2, p 3 } oletus. 5 { p 1 } (1, 2) 7 (4, 6)

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

Lisää kvanttoreista ja päättelyä sekä predikaattilogiikan totuustaulukot 1. Negaation siirto kvanttorin ohi

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

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

Luonnollisen päättelyn luotettavuus

Logiikan kertausta. TIE303 Formaalit menetelmät, kevät Antti-Juhani Kaijanaho. Jyväskylän yliopisto Tietotekniikan laitos.

TIEA341 Funktio-ohjelmointi 1, kevät 2008

11/20: Konepelti auki

Todistusteoriaa. Kun kielen syntaksi on tarkasti määritelty, voidaan myös määritellä täsmällisesti, mitä pätevällä päättelyllä tarkoitetaan.

Kesälukio 2000 PK2 Tauluharjoituksia I Mallivastaukset

Lisää pysähtymisaiheisia ongelmia

Zeon PDF Driver Trial

Matematiikan tukikurssi, kurssikerta 2

Reaalifunktioista 1 / 17. Reaalifunktioista

Miten osoitetaan joukot samoiksi?

Matematiikan johdantokurssi, syksy 2016 Harjoitus 11, ratkaisuista

Esitetään tehtävälle kaksi hieman erilaista ratkaisua. Ratkaisutapa 1. Lähdetään sieventämään epäyhtälön vasenta puolta:

MS-A0402 Diskreetin matematiikan perusteet Esimerkkejä, todistuksia ym., osa I

MS-A0402 Diskreetin matematiikan perusteet Esimerkkejä, todistuksia ym., osa I

Algoritmit 1. Luento 3 Ti Timo Männikkö

Loogiset konnektiivit

MS-A0402 Diskreetin matematiikan perusteet Esimerkkejä ym., osa I

FI3 Tiedon ja todellisuuden filosofia LOGIIKKA. 1.1 Logiikan ymmärtämiseksi on tärkeää osata erottaa muoto ja sisältö toisistaan:

Karteesinen tulo. Olkoot A = {1, 2, 3, 5} ja B = {a, b, c}. Näiden karteesista tuloa A B voidaan havainnollistaa kuvalla 1 / 21

Jokaisen parittoman kokonaisluvun toinen potenssi on pariton.

LOGIIKKA johdantoa

Nimitys Symboli Merkitys Negaatio ei Konjuktio ja Disjunktio tai Implikaatio jos..., niin... Ekvivalenssi... jos ja vain jos...

Matematiikan tukikurssi, kurssikerta 3

Haskell ohjelmointikielen tyyppijärjestelmä

HY / Matematiikan ja tilastotieteen laitos Johdatus logiikkaan I, syksy 2018 Harjoitus 5 Ratkaisuehdotukset

Todistusmenetelmiä Miksi pitää todistaa?

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

MS-A0402 Diskreetin matematiikan perusteet Esimerkkejä ym., osa I

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

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

f(n) = Ω(g(n)) jos ja vain jos g(n) = O(f(n))

Tietorakenteet ja algoritmit - syksy

811120P Diskreetit rakenteet

T Logiikka tietotekniikassa: perusteet Kevät 2008 Laskuharjoitus 5 (lauselogiikka ) A ( B C) A B C.

Tietorakenteet ja algoritmit Johdanto Lauri Malmi / Ari Korhonen

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

TIEA341 Funktio-ohjelmointi 1, kevät 2008

MS-A0402 Diskreetin matematiikan perusteet

Johdatus matemaattiseen päättelyyn

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

58131 Tietorakenteet ja algoritmit (syksy 2015)

815338A Ohjelmointikielten periaatteet Harjoitus 3 vastaukset

14.1 Rekursio tyypitetyssä lambda-kielessä

verkkojen G ja H välinen isomorfismi. Nyt kuvaus f on bijektio, joka säilyttää kyseisissä verkoissa esiintyvät särmät, joten pari

Rekursiiviset palautukset [HMU 9.3.1]

Matematiikan tukikurssi

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

Matematiikan peruskurssi 2

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

b) Määritä myös seuraavat joukot ja anna kussakin tapauksessa lyhyt sanallinen perustelu.

1. Otetaan perusjoukoksi X := {0, 1, 2, 3, 4, 5, 6, 7}. Piirrä seuraaville kolmelle joukolle Venn-diagrammi ja asettele alkiot siihen.

LOAD R1, =2 Sijoitetaan rekisteriin R1 arvo 2. LOAD R1, 100

Ohjelmoinnin perusteet Y Python

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

Modus Ponens. JosAjaA B ovat tosia, niin välttämättä myösb on tosi 1 / 15. Modus Ponens. Ketjusääntö. Päättelyketju.

Johdatus lukuteoriaan Harjoitus 2 syksy 2008 Eemeli Blåsten. Ratkaisuehdotelma

JFO: Johdatus funktionaaliseen ohjelmointiin

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

Sekalaiset tehtävät, 11. syyskuuta 2005, sivu 1 / 13. Tehtäviä

Rajoittamattomat kieliopit (Unrestricted Grammars)

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

Tehtäväsarja I Seuraavissa tehtävissä harjoitellaan erilaisia todistustekniikoita. Luentokalvoista 11, sekä voi olla apua.

MAT Laaja Matematiikka 1U. Hyviä tenttikysymyksiä T3 Matemaattinen induktio

Ratkaisu: (b) A = x 0 (R(x 0 ) x 1 ( Q(x 1 ) (S(x 0, x 1 ) S(x 1, x 1 )))).

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

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

Muita vaativuusluokkia

ITKP102 Ohjelmointi 1 (6 op)

1. Universaaleja laskennan malleja

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

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

Tenttiin valmentavia harjoituksia

Diskreetin matematiikan perusteet Laskuharjoitus 1 / vko 8

Tietorakenteet (syksy 2013)

Transkriptio:

85 Takarekursiosta Sanoimme luvun 83 foldl -esimerkissämme että foldl :: (a -> b -> a) -> a -> [b] -> a foldl f z [] = z foldl f z (x:xs) = foldl f (f z x) xs olisi pelkkä silmukka Tämä johtuu siitä, että sen rekursiivinen kutsu on erityistä muotoa: takarekursiota (tail recursion) Kutsu on funktion koodissa takapaikalla (in tail position, tail call) jos se on viimeinen askel jonka tämä funktionkoodihaara ottaa Tässä rekursiivinen foldl-kutsu on takapaikalla: Kun siitä rekursiokutsusta palataan, niin sen jälkeen tämä foldl-kutsu ei enää tee mitään muuta kuin vain palauttaa sieltä saamansa arvon omalle kutsujalleen Silloin kuin takapaikalla kutsutaan funktiota itseään, niin kyseessä on takarekursiivinen kutsu kuten siis tässä Takapaikalla olevat kutsut ovat mielenkiintoisia koska ohjelmointikielen toteutus voi optimoida niitä (tail call optimization, TCO): Toteutus ylläpitää tyypillisesti pinoa jonka avulla tiedetään, mihin pitää palata jatkamaan suoritusta, kun tämän nykyisen aliohjelman suoritus päättyy Tavallinen kutsu lisää ensin tähän pinoon nämä riittävät paluutiedot ennen kuin siirtyy kutsuttavaan aliohjelmaan, jotta se aikanaan osaa palata takaisin tänne mistä sitä kutsuttiin Siis tavallinen kutsu kasvattaa tätä pinoa Takapaikalla oleva kutsu voidaankin sen sijaan toteuttaa kasvattamatta tätä pinoa: Takapaikalla kutsuttava aliohjelma voikin palata suoraan sinne, johon tämä aliohjelma itse palaisi joka tapauksessa heti tämän viimeisen aliohjelmakutsunsa jälkeen lukemalla suoraan sen paluutiedot Koska funktionaalisissa kielissä kuten ei ole erillisiä toistorakenteita kuten whilesilmukkaa, vaan toistokin on rekursiota, niin niissä TCO on välttämätöntä muuten while-simukan funktionaalinen toteutus täyttäisi hiljalleen koko muistin pinon kasvaessa kierros kierrokselta Aiemmassa luvun 72 esimerkissämme kutsuimme funktiota laskin takarekursiivisesti TCO ja muistin ahkeruus takaavat, että se voisi jatkaa mielivaltaisen pitkään täyttämättä koneen muistia Näimme yhtälössä (16) TCO:n vaikutuksen silloin, kun laskentaa tarkastellaan sieventämisenä: Lausekkeessa oli koko ajan vain yksi foldl jonka parametrit muuttuivat eli laskenta pyöri foldl-silmukassa 130

Verrataan sitä aidosti rekursiiviseen foldr :: (a -> b -> b) -> b -> [a] -> b foldr f z [] = z foldr f z (x:xs) = f x (foldr f z xs) jolla laskenta etenisikin foldr (+) 0 [x 1, x 2, x 3,,x n ] = βx 1 + (foldr (+) 0 [x 2, x 3,,x n ]) = βx 1 + (x 2 +foldr (+) 0 [x 3,,x n ]) = βx 1 + (x 2 + (x 3 + ( + (x n + (foldr (+) 0 []))))) = βx 1 + (x 2 + (x 3 + ( + (x n + 0)))) Tässä foldr-laskennassa seuraava redex menee yhä syvemmälle ja syvemmälle tuloslausekkeessa Tämä polku seuraavaan redeksiin (tai paremminkin: polku sieltä takaisin) on se, jota ylläpidetään pinossa Yhtälön (16) foldl-laskennassa seuraava redex pysyi sen sijaan aina lausekkeen alussa Kun vielä tehostimme tämän laiskan foldl-laskennan ahkeraksi foldl -laskennaksi, niin saimme haluamamme vakiomuistitilassa pyörivän silmukan Tällaisen foldr-laskennan muistintarve ei riipu funktion f laiskuudesta tai ahkeruudesta, vaan siitä että f joutuu odottamaan foldr-rekursiokutsun tulosta Toisaalta laiskassa foldr-laskennassa f voikin jossakin vaiheessa päättää, ettei se tarvitsekaan foldr-rekursiokutsun tulosta, ja siten lopettaa syötelistan läpikäynnin jo ennen sen päättymistä Tähän foldl-laskenta taas ei pysty, vaan sen on aina käytävä syötelistansa loppuun saakka Laiskassa funktionaalisessa ohjelmoinnissa käytetäänkin tavallisesti funktiota foldr elleivät ajan- tai muistinkäyttö vaadi funktion foldl tai foldl käyttöä Eräs usein toimiva tapa saada aidosti rekursiivisesta koodista takarekursiivista on lisätä siihen sopiva kerääjäparametri Aiemmin luvussa 412 lähdimme optimoimaan funktiota, jossa oli kaksi aitoa rekursiokutsua: Ensin syötteenä saadun hakupuun vasen ja oikea alipuu käsiteltiin rekursiivisesti Sitten näiden rekursiokutsujen antamat osatulokset vielä yhdistettiin operaatiolla (++) koko tulokseksi Lisäämällä kerääjäparametrin a saimme optimoitua sen sellaiseen muotoon, jossa oikea alipuu käsiteltiin yhä aidolla rekursiolla, mutta vasen takarekursiolla Siis silmukaksi joka etenee hakupuun vasenta haaraa pitkin ja käsittelee mennessään sen oikeat haarat rekursiivisesti 131

Samassa yhteydessä huomautimme myös, että foldl voidaan nähdä funktion foldr kerääjäparametriversiona silloin kun f on liitännäinen ja z sen neutraalialkio Yhtälössä (16) taas nähtiin, että tällainen kerääjäparametri kannattaa usein laskea ahkerasti, jotta saavutettaisiin oikeasti se haluttu muistinsäästö Siten funktiosta foldl saatiin foldl 86 Tyypinpäättelyn periaatteista Haskell sisältää siis ns Hindley-Milner-tyypinpäättelyn, johon on lisätty tyyppiluokat Tutustutaan nyt sen perusperiaatteisiin yleisellä tasolla Tutustutaan myös tyypinpäättelyn ja loogisen päättelyn välisiin yhteyksiin hyvin yleisellä tasolla Otetaan Preludesta esimerkiksemme listan pituuden laskeva funktio length [] = 0 length (_:l) = 1 + length l Haskell-ohjelmoija voisi päätellä sen tyypin intuitiivisesti vaikkapa seuraavasti: Tämä length saa yhden parametrin, eli se on tyyppiä funktio parametrinsa tyypiltä a tuloksensa tyypille b jotka selviävät tarkemmin myöhemmin Koska Numero 0 on yksi sen mahdollinen tulos, niin tämän tyypin b pitää olla jokin Numerotyyppi eli Num b Koska tyhjä lista [] on yksi mahdollinen arvo sen parametrille, niin tämän tyypin a pitää olla muotoa [c] Koska length ei välitä syötelistansa alkioista, sillä alkion kohdalla on hahmossa _, niin tämä alkiotyyppi c saa olla mitä tahansa eli se saa olla monimuotoinen Siis kaiken kaikkiaan tyypiksi saadaan length :: (Num b) => [c] -> b Prelude antaa sille hieman täsmällisemmän tyypin length :: [c] -> Int jossa b = Int koska se riittää yleensä listan pituudelle on nopeampi käsitellä kuin mielivalintainen Numero Vakiokirjastossa on myös tämä yleisempi funktio nimellä DataListgenericLength Samoin sieltä löytyy myös DataListgenericTake jne 132

Tehdään sitten vastaava päättely hieman tarkemmin siten kuin Haskell sen voisi suorittaa Otetaan käyttöön logiikasta (tuttu?) notaatio oletukset lausekkeen osien tyypeistä johtopäätös koko lausekkeen tyypistä niille säännöille, joita tyypinpäättelyssä käytetään Olemme jo nähneet tällaisia sääntöjä epäformaalisti eri lausekkeiden yhteydessä Esimerkiksi lausekkeessa case valinta of hahmo 1 -> tulos 1 hahmo k -> tulos k vaadittiin, että 1 valinta lausekkeen ja kaikkien hahmojen pitää olla keskenään samaa tyyppiä 2 valinta lausekkeiden pitää olla keskenään samaa tyyppiä, ja niiden yhteinen tyyppi on myös koko case-lausekkeen tyyppi Tämä voidaan ilmaista sääntönä h 1 :: t h k :: t v, h 1,,h k :: t jossa t on ehdon 1 ja u ehdon 2 tyyppi w 1 :: u w k :: u case v of {h 1 ->w 1 ; ;h k ->w k } :: u (17) Merkintä h i :: t w i :: u tarkoittaa että jos tehdään lisäoletus h i :: t niin silloin siitä voidaan todistaa johtopäätös w i :: u eli että näiden kolmen pisteen kohdalle voidaan laatia jokin todistus Vasemmanpuoleiset oletukset taas takaavat, että nämä lisäoletukset ovat voimassa joten nämä johtopäätöksetkin ovat voimassa Esimerkkimme alkaa tyypitettävän funktion sisäisestä esitysmuodosta ilman syntaktista sokeria: length = \ p -> case p of 133

[] -> 0 _:xs -> ((+) 1) (length xs) Voidaan aloittaa funktioiden säännöllä Tällaista sääntöä voidaan käyttää x :: t e :: u (\ x -> e) :: t -> u (18) edeten säännössä alhaalta ylös eli johtopäätöksestä oletuksiin koristellen funktiota säännön mukaisilla tyyppitarkenteilla :: Tyyppi Haskell salliikin tällaisten tyyppitarkenteiden kirjoittamisen varsin moneen kohtaan lähdekoodia Niillä ohjelmoija voi määrätä tyypin tarkemmaksi kuin se, jonka Haskell päättelee ei tietenkään väljemmäksi, sehän vaarantaisi tyyppiturvallisuuden! Käytimme sitä luvun 524 Isqrt-tyyppiluokkaesimerkissämme takaamaan, että laitetason neliöjuuri sqrt lasketaan tarkemmilla liukuluvuilla Double Näin saadaan length :: a -> b = \ (p :: a) -> (case p of [] -> 0 _:xs -> (((+) 1) (length xs))) :: b jossa a, b, c, ovat vielä tällä hetkellä tuntemattomia, eli uusia tyyppimuuttujia, eli sellaisia joita ei vielä ole käytetty Sitten voimme siirtää tämän b case-lausekkeen sisään vastaavalla säännöllä (17): length :: a -> b = \ (p :: a) -> case (p :: a) of [] :: a -> 0 :: b (_:xs) :: a -> (((+) 1) (length xs)) :: b 134

Tämän casen valinta lausekkeen p tyypiksi on jo edellisessä vaiheessa sovittu a Laajennetaan tämä sopimus myös sen hahmoihin Tiedämme, että 0 on Numerovakio: jonka perusteella length :: (Num b) => a -> b = \ (p :: a) -> case (p :: a) of [] :: a -> 0 :: b (_:xs) :: a -> (((+) 1) (length xs)) :: b 0 :: (Num t) => t joka tuo mukanaan lisärajoitteen Num b eli tyypin b pitää olla Numeerinen Tämä lisärajoite liittyy siis joka tyyppiin, jossa tyyppimuuttuja b esiintyy, mutta nyt merkitään se vain ensimmäiseen sellaiseen tyyppiin eikä toisteta sitä enää muualla Tarkemmin sanoen tapahtuu seuraavaa: Vakion 0 sisäinen sokeroimaton muoto onkin (frominteger :: (Num b) => Integer -> b) (0 :: Integer) eli Integer-vakio 0 ja sen konversio mihin tahansa Numerotyyppiin Kun siihen sovelletaan funktion kutsun tyypityssääntöä sen tyypiksi saadaan se väitetty (Num b) => b f :: t -> u x :: t f x :: u (19) Tiedämme myös, että tyhjä lista [] on vakio jonka alkiotyyppi t on monimuotoinen [] :: [t] Sen perusteella tyyppi a on siis listatyyppi [c] jossa emme tunne alkiotyyppiä c: 135

length :: (Num b) => [c] -> b = \ (p :: [c]) -> case (p :: [c]) of [] :: [c] -> 0 :: b (_:xs) :: [c] -> (((+) 1) (length xs)) :: b Listahahmoilla on tyypityssääntö jonka perusteella length :: (Num b) => [c] -> b = \ (p :: [c]) -> case (p :: [c]) of [] :: [c] -> 0 :: b (_ :: c) : (xs :: [c]) -> (((+) 1) (length xs)) :: b y :: c z :: [c] y:z :: [c] Nyt on jäljellä enää viimeisen rivin käsittely Se koostuu funktionkutsuista, ja ne siis käsitellään tyypityssäännöllä (19) Etenemme ulkoa sisään koska tiedämme koko lausekkeen tyypin olevan b, ja haluamme viedä tämä tiedon sen osiin Ulommaisen kutsun käsittely tuottaa joten uusi tyyppi e onkin vanha tyyppi [c] d onkin vanha tyyppi b length :: e -> d xs :: e ((+) 1) :: d -> b (length xs) :: d (((+) 1) (length xs)) :: b aiempien tyypitysvaiheiden perusteella Siten jäljelle jää pääteltäväksi enää jonka oletuksista (+) :: f -> (b -> b) 1 :: f ((+) 1) :: b -> b vasen antaa metodin (+) tyypin nojalla että uusi tyyppi f onkin sama kuin vanha tyyppi b oikea voidaan käsitellä kuten vakio 0 136

Yhteenvetona siis length :: (Num b) => [c] -> b = \ (p :: [c]) -> case (p :: [c]) of [] :: [c] -> 0 :: b (_ :: c) : (xs :: [c]) -> ((((+) :: b -> (b -> b)) (1 :: b)) ((length :: [c] -> b) (xs :: [c]))) :: b Lopuksi tehdään vielä yleistysaskel (generalization): Koska tyypitys on nyt saatettu onnistuneesti loppuun, niin jätetään jäljelle jääneet tyyppimuuttujat b ja c monimuotoisiksi kunhan muistetaan säilyttää mukana myös se lisärajoite, että Num b Jos käyttäisimme Haskell-laajennuksena olevaa tyyppikvanttoria niin voisimme esittää tämän yleistysaskeleen sen tuomisena koko tyypin eteen: length :: forall b c (Num b) => [c] -> b Parametrinen monimuotoisuus tarkoittaakin että mikä tahansa tyyppi käy, koska tyypityksen ei tarvinnut olettaa siitä tämän enempää Prelude siis antaa tälle funktiolle tarkemman tyypin length :: [c] -> Int joka tarkentaa tätä pääteltyä tulosta siten, että b = Int: length :: [c] -> Int = \ (p :: [c]) -> case (p :: [c]) of [] :: [c] -> 0 :: Int (_ :: c) : (xs :: [c]) -> ((((+) :: Int -> (Int -> Int)) (1 :: Int)) ((length :: [c] -> Int) (xs :: [c]))) :: Int Lisärajoite saa muodon Num Int joka on totta ja voidaan poistaa: on olemassa vastaava instance Num Int Samoin aiemmin monimuotoinen metodin (+) kutsu on nyt täsmentynyt kutsumaan juuri sitä versiota, joka löytyy juuri tästä instanssista 137

Esimerkissämme ei ollut paikallisia määrittelyjä Lauseke let määrittelyt in runko tyypitetään seuraavasti: 1 Ensin tyypitetään nämä paikalliset määrittelyt kuten edellä Jos sen kuluessa todetaan, että kaksi tyyppimuuttujaa (vaikkapa x ja y) ovat itse asiassa sama tyyppi, ja niistä yksi (vaikkapa x) on kotoisin tämän let-lausekkeen ulkopuolelta toinen (vaikkapa y) syntyi vasta näitä paikallisia määrittelyjä tyypittäessä niin silloin jälkimmäinen korvataan edellisellä eikä päinvastoin (eli y korvataan muuttujalla x eikä päinvastoin) eli uudempi vanhemmalla 2 Sitten yleistetään monimuotoisiksi vain ne uudet tyyppimuuttujat, jotka syntyivät tyypitysaskeleen 1 aikana mutta joita ei korvattu vanhemmilla 3 Lopuksi tyypitetään runko käyttäen näitä paikallisia ja yleistettyjä määrittelyjä Näin saatu rungon tyyppi on samalla koko let-lausekkeen tyyppi Myös while-lauseke tyypitetään näin Tällainen tyypinpäättely on ns PSPACE-täydellistä (PSPACE-complete) eli intuitiivisesti yksi vaikeimmista laskentaongelmista, joka voidaan ratkaista järkevällä määrällä muistia mutta johon voi silti kulua järjettömän paljon aikaa Kaikki kurssilla LAP esitellyt NP-täydelliset laskentaongelmat kuuluvat myös tähän luokkaan PSPACE, ja yleisesti uskotaan että PSPACE sisältää paljon muutakin kuin ne Onneksi tällainen tyypinpäättely on osoittautunut olevan nopeaa ohjelmakoodin pituuteen verrattuna ja hidasta vain sen sisennyssyvyyteen eli leveyteen verrattuna ja onneksi ohjelmat ovat tavallisesti hyvin pitkiä mutteivät kovinkaan leveitä 87 Ohjelmoinnin ja logiikan suhteesta Edellä luvussa 86 on kuvattu Haskellin tyypinpäättelyä samaan tapaan kuin tehtäisiin päättelyä logiikassa Niillä onkin läheinen yhteys; tarkastellaan sitäkin hieman Tarkastellaan sellaisia Haskell-lausekkeita, joissa ei käytetä paikallisia let- eikä where-määrittelyjä ei käytetä arvoa undefined ainoana perustyyppinä on vain () muista monikoista käytetään monikoista vain pareja 138

algebrallisista tyypeistä käytetään vain tyyppiä data Either a b = Left a Right b ja tarkastellaan niiden tyypittämistä Tämä yhteys on nähty alun perin λ-lausekkeilla, mutta pysymme nyt niiden sijaan Haskell-lausekkeissä Parien tyypityssääntö on x :: s y :: t (x,y) :: (s,t) (20) ja parityypeille case-sääntö (17) saa muodon v :: (s,t) x :: s y :: t x :: s, y :: t w :: u case v of {(x,y)->w} :: u (21) mutta tavallisesti käytetään Preludestakin löytyviä funktioita fst v = case v of {(x,_)->x} snd v = case v of {(_,y)->y} Either-tyyppikonstruktorilla on kaksi sääntöä, yksi sen kummallekin arvokonstruktorille: x :: s Left x :: Either s t (22) y :: t Right y :: Either s t (23) Tämän tyyppikonstruktorin case-sääntö (17) saa muodon v :: Either s t x :: s y :: t x :: s w L :: u y :: t w R :: u case v of {Left x->w L ;Right y->w R } :: u (24) mutta tavallisesti käytetään Preludestakin löytyvää funktiota either :: (s -> u) -> (t -> u) -> Either s t -> u either f g v = case v of {Left x->f x;right y->g y} Unohdetaan hetkeksi tyypityssäännöistämme osat jos lauseke on tyyppiä eli osat lauseke :: Mitä jää? (20): Jos on todistettu sekä s että t niin voidaan todistaa myös (s,t) Tämähän on loogisen konjunktion sääntö kunhan vain kirjoitetaan(s,t) muodossa s t 139

Ohjelmoijan intuitio onkin että parissa on yksi kenttä tyyppiä s ja toinen tyyppiä t Tästä näkökulmasta fst esittää päättelysäännön s t s eli jos on todistettu konjunktio, niin on todistus myös sen ensimmäiselle konjunktille Vastaavasti snd on toisen konjunktin t päättelysääntö Näistä case-säännöistä jätetään lukematta myös niiden hahmojen tyypitykset, ja luetaankin vain lausekkeiden tyypitykset Tyyppi () on siis tyhjä konjunktio eli tosi (18): Jos oletuksella t voitiin todistaa u niin voidaan todistaa myös t -> u Tämähän on loogisen implikaation sääntö kunhan vain kirjoitetaan t -> u muodossa t u Funktioihin ei voi soveltaa case-lausekkeita mutta sen sijaan niitä voi kutsua: Kutsun tyypityssääntö (19) voidaan lukea jos on todistettu sekä t -> u että t niin voidaan todistaa myös u Tämähän on puolestaan päättelysääntö modus ponens eli implikaation käyttö : t u t u (25) (22): Jos on todistettu s niin voidaan todistaa myös Either s t Tämähän on loogisen disjunktion yksi sääntö kunhan vain kirjoitetaan Either s t muodossa s t Vastaavasti (23) on se toinen sääntö, joka todistaakin saman disjunktion jos t onkin todistettu Ohjelmoijan intuitio onkin että tässä tietueessa on joko kenttä tyyppiä s tai kenttä tyyppiä t, ja lisäksi tieto kumpi näistä kentistä siinä on Vastaava case-lauseke (24) on puolestaan päättelysääntö s t s u t u u eli jos on todistettu että s tai t ja että kummastakin seuraa u niin sitten voidaan todistaa myös u eli funktio either Siten tyypityssääntömme Tyyppi osat muodostavatkin propositionaalisen eli lauselogiikan (propositional logic) Tämä logiikka ei kuitenkaan olekaan se tuttu klassinen (classical) joka puhuu totuudesta, vaan konstruktiivinen (constructive) joka puhuukin todistuvuudesta Konstruktiivista logiikkaa nimitetään myös intuitionistiseksi (intuitionistic) Siis tyyppi onkin konstruktiivisen lauselogiikan kaava Väite Marsissa joko on elämää tai siellä ei ole elämää on 140

tosi klassisesti koska sen totuusarvo ei riipu Marsin todellisuudesta avoin konstruktiivisesti koska meillä ei ole vielä todistusta kummallekaan sen osaväitteelle Siis kolmannen poissuljetun laki φ φ on tautologia(na pätevä) klassisessa logiikassa mutta ei konstruktiivisessa Konstruktiivinen logiikka antaakin omalle implikaatiolleen Brouwer-Heyting- Kolmogorov-tulkinnan: Implikaatio t u pätee konstruktiivisesti, jos on olemassa jokin tapa muuntaa mikä tahansa todistus T kaavalle t todistukseksi U kaavalle u konstruoida U todistuksesta T Brouwer kehitti matematiikan perusteiden filosofiaa 1900-luvun alussa Heyting taas kehitti silloin sellaisen algebran, joka on konstruktiivisen logiikan taustalla samaan tapaan kuin binäärialgebra on klassisen logiikan taustalla Kolmogorov oli monipuolinen matemaatikko: tätä logiikkaa, todennäköisyyslaskennan aksiomatisointi, kompleksisuuden mitta, Klassisessa logiikassa taas implikaatio oli sama kuin mutta ei siis konstruktiivisessa φ ψ φ ψ Lausekkeet tyyppiä t ja u voidaan puolestaan nähdä esitystapoina näille todistuksille T ja U: Funktiot fst, snd ja either edustavat päättelysääntöjä, samoin funktion määrittely ja sen kutsu Siten lauseke onkin todistus tyyppinsä ilmaisemalle konstruktiivisen lauselogiikan kaavaille ja tyypintarkistus onko T :: t? todistuksen T tarkistamista ja halutun tyyppisen lausekkeen ohjelmointi onkin tällaisen todistuksen laatimista! Silloin BHK-tulkinnan vaatimaksi tavaksi puolestaan käy mikä tahansa lauseke, jonka tyyppi on F :: t u koska silloin lauseke F T antaa sopivan tuloksen U: Koska lausekkeissamme ei ole rekursiota, niin onnistunut tyypitys takaa normalisoinnin pysähtymisen Lausekkeissamme ei ole myöskään mitään, joka johtaisi suoritusaikaiseen virheeseen Siten funktion arvon laskenta eli ohjelman suoritus onkin tällaisen todistuksen sieventämistä kohden normaalimuotoaan, jossa sen sisältämät päättelyaskeleet kuten erityisesti (25) onkin oikaistu 141

Yhteenvetona edellä (hyvin epätarkasti ) luonnosteltu Curryn-Howardin vastaavuus (the Curry-Howard correspondence, the proofs-as-programs correspondence) on: käsite ohjelmoinnissa tyyppi jokin sen tyyppinen lauseke sen tyyppisen lausekkeen arvon laskenta eli suorittaminen eli sieventäminen halutun tyyppisen lausekkeen ohjelmointi näin ohjelmoidun lausekkeen tyypin tarkistus parametrinen monimuotoisuus sama käsite logiikassa kaava jokin sitä tyyppiä vastaavan kaavan todistus vastaavan todistuksen yksinkertaistaminen todistuksen laatiminen sen tyyppiä vastaavalle kaavalle näin laaditun todistuksen tarkistaminen yleistys kaikilla kaavoilla pätee, että Tämä vastaavuus inspiroi ohjelmoinnin ja ohjelmointikielten teoriaa kehittämään yhä ilmaisuvoimaisempia tyyppijärjestelmiä jotta tyypeillä voitaisiin ilmaista yhä enemmän ohjelmien spesifikaatioiden sisältämästä informaatiosta menetelmiä, joilla formaaleista loogisista todistuksista voisi generoida niihin liittyvän ohjelmakoodin (puoli)automaattisesti Silloin ohjelmoijasta tulisikin loogikko, joka todistaa konstruktiivisesti, että tyypeillä/kaavoilla esitetty spesifikaatio on todellakin mahdollista toteuttaa ja tietokone generoisi automaattisesti hänen todistuksestaan sitä vastaavan eli spesifikaation mukaisen ohjelmatoteutuksen Käytännössä tällainen äärimmäisen formaali korrektiuteen keskittyvä ohjelmointitapa rajoittuisi varmaankin sellaisiin ohjelmistoihin ja ohjelmistojen ydinosiin, joiden luotettavuus on aivan välttämätöntä Ehkäpä jonain päivänä 142