7.5 Monadimuuntimet. Vaikka monadinen tyypitus on monoliittista, niin monadit voi suunnitella ja toteuttaa hierarkisesti.

Samankaltaiset tiedostot
Jäsennys. TIEA341 Funktio ohjelmointi 1 Syksy 2005

TIEA341 Funktio-ohjelmointi 1, kevät 2008

5.5 Jäsenninkombinaattoreista

Algebralliset tietotyypit ym. TIEA341 Funktio ohjelmointi 1 Syksy 2005

815338A Ohjelmointikielten periaatteet Harjoitus 2 vastaukset

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

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

Jäsennysalgoritmeja. TIE448 Kääntäjätekniikka, syksy Antti-Juhani Kaijanaho. 29. syyskuuta 2009 TIETOTEKNIIKAN LAITOS. Jäsennysalgoritmeja

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

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

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

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

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

follow(a) first(α j ) x

815338A Ohjelmointikielten periaatteet Harjoitus 6 Vastaukset

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

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

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

jäsentäminen TIEA241 Automaatit ja kieliopit, syksy 2015 Antti-Juhani Kaijanaho 26. marraskuuta 2015 TIETOTEKNIIKAN LAITOS

jäsentämisestä TIEA241 Automaatit ja kieliopit, syksy 2015 Antti-Juhani Kaijanaho 27. marraskuuta 2015 TIETOTEKNIIKAN LAITOS

Attribuuttikieliopit

TIEA241 Automaatit ja kieliopit, kevät 2011 (IV) Antti-Juhani Kaijanaho. 29. huhtikuuta 2011

jäsennyksestä TIEA241 Automaatit ja kieliopit, syksy 2016 Antti-Juhani Kaijanaho 29. syyskuuta 2016 TIETOTEKNIIKAN LAITOS Kontekstittomien kielioppien

ICS-C2000 Tietojenkäsittelyteoria Kevät 2016

Yhteydettömän kieliopin jäsennysongelma

TIEA241 Automaatit ja kieliopit, kevät Antti-Juhani Kaijanaho. 12. kesäkuuta 2013

815338A Ohjelmointikielten periaatteet Harjoitus 7 Vastaukset

Ohjelmoinnin perusteet Y Python

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

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

Haskell ohjelmointikielen tyyppijärjestelmä

Luku 3. Listankäsittelyä. 3.1 Listat

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

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

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

Laskennan mallit (syksy 2010) Harjoitus 8, ratkaisuja

Lisää pysähtymisaiheisia ongelmia

ITKP102 Ohjelmointi 1 (6 op)

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Yhteydettömät kieliopit [Sipser luku 2.1]

6 Algebralliset tietotyypit

TIEA341 Funktio-ohjelmointi 1, kevät 2008

TIEA241 Automaatit ja kieliopit, kevät Antti-Juhani Kaijanaho. 16. helmikuuta 2012

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

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

11.4. Context-free kielet 1 / 17

TIEA341 Funktio-ohjelmointi 1, kevät 2008

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

Abstraktit tietotyypit. TIEA341 Funktio ohjelmointi 1 Syksy 2005

Ongelma(t): Miten jollakin korkeamman tason ohjelmointikielellä esitetty algoritmi saadaan suoritettua mikro-ohjelmoitavalla tietokoneella ja siinä

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

5.3 Laskimen muunnelmia 5.3. LASKIMEN MUUNNELMIA 57

on rekursiivisesti numeroituva, mutta ei rekursiivinen.

Luonnollisen päättelyn luotettavuus

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

TIES542 kevät 2009 Tyyppijärjestelmän laajennoksia

Sisällys. 12. Näppäimistöltä lukeminen. Yleistä. Yleistä

Ydin-Haskell Tiivismoniste

M =(K, Σ, Γ,, s, F ) Σ ={a, b} Γ ={c, d} = {( (s, a, e), (s, cd) ), ( (s, e, e), (f, e) ), (f, e, d), (f, e)

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

811120P Diskreetit rakenteet

Ohjelmoinnin perusteet Y Python

Algoritmit 1. Luento 3 Ti Timo Männikkö

Operaattoreiden ylikuormitus. Operaattoreiden kuormitus. Operaattoreiden kuormitus. Operaattoreista. Kuormituksesta

Zeon PDF Driver Trial

ELM GROUP 04. Teemu Laakso Henrik Talarmo

Ohjelmoinnin perusteet Y Python

Ohjelmoinnin perusteet Y Python

ITKP102 Ohjelmointi 1 (6 op)

ICS-C2000 Tietojenkäsittelyteoria Kevät 2016

Luento 5. Timo Savola. 28. huhtikuuta 2006

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

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

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

Taas laskin. TIES341 Funktio ohjelmointi 2 Kevät 2006

uv n, v 1, ja uv i w A kaikilla

Säännöllisten kielten sulkeumaominaisuudet

1. Universaaleja laskennan malleja

Ohjelmoinnin peruskurssien laaja oppimäärä

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

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Ohjelmoinnin perusteet Y Python

Rekursiiviset palautukset [HMU 9.3.1]

ITKP102 Ohjelmointi 1 (6 op)

S BAB ABA A aas bba B bbs c

TIEA241 Automaatit ja kieliopit, kevät Antti-Juhani Kaijanaho. 2. helmikuuta 2012

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

Rajoittamattomat kieliopit (Unrestricted Grammars)

Matematiikan tukikurssi, kurssikerta 2

12. Näppäimistöltä lukeminen 12.1

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

Rekursiivinen Derives on periaatteessa aivan toimiva algoritmi, mutta erittäin tehoton. Jos tarkastellaan esim. kieliopinpätkää

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

vaihtoehtoja TIEA241 Automaatit ja kieliopit, syksy 2016 Antti-Juhani Kaijanaho 13. lokakuuta 2016 TIETOTEKNIIKAN LAITOS

TIEA341 Funktio-ohjelmointi 1, kevät 2008

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

FORMAALI SYSTEEMI (in Nutshell): aakkosto: alkeismerkkien joukko kieliopin määräämä syntaksi: sallittujen merkkijonojen rakenne, formaali kuvaus

Ohjelmassa henkilön etunimi ja sukunimi luetaan kahteen muuttujaan seuraavasti:

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

7/20: Paketti kasassa ensimmäistä kertaa

Transkriptio:

7.5 Monadimuuntimet Vaikka monadinen tyypitus on monoliittista, niin monadit voi suunnitella ja toteuttaa hierarkisesti. Sitä varten ghc tarjoaa monadimuuntimet (monadic transformers) (O Sullivan et al., 2008, luku 18). Jos M ja N ovat monadeja, niin N (M a) on sellaisen monadin tyyppi, jossa ulompi monadi N tuottaa vastauksen tyyppiä M a jossa sisempi monadi M esittää sellaisen laskennan, jonka vastaus on a. Silloin tämä ulompi tyyppi N onkin sellainen, joka ottaa sisäänsä toisen monadin M antaa tuloksenaan uuden monadin, jossa sisään otettua monadia M on täydennetty uusilla ominaisuuksilla. Eri monadimuuntimet N voivat siten esittää eri täydennystapoja, ja haluttu monadi koota niitä pinoamalla B (...(O (N (M a)))...). Kun laskennassa halutaan käyttää jotakin sisämonadin operaatiota niin sen käyttö tulos <- operaatio kirjoitetaankin muodossa tulos <- lift $ operaatio jossa tämän monadimuuntimen t metodi lift :: (Monad m, MonadTrans t) => m a -> t m a nostaa tämän operaation sisämonadista m=m ulkomonadin operaatioksi. ulkomonadin operaatiota niin se on käytössä suoraan ilman nostamista. Siten monadimuuntimen t käyttö koostuu sellaisen tyypin kirjoittamisesta, jossa uloimpana on t ja sisällä sisämonadin tyyppi sisämonadista tarvittavien operaatioiden nostamisesta. ghc tarjoaa Control.Monad-kirjastoissaan monia tällaisia muuntimia. Esimerkkinä täydennetään monadia ST virheenkäsittelymekanismilla, jonka tarjoaa monadimuunnin ErrorT kirjastosta Control.Monad.Error. Ulkomonadimme tyyppi on muotoa ErrorT virheilmoitustyyppi sisämonadi vastaustyyppi Me käytämme virheilmoitustyyppinämme Stringiä. Meidän sisämonadimme on ST s jossa s on se eksistentiaalikvantifioitava tyyppiparametri. 122

Meidän tyyppimme omat parametrit ovat tämä s ja sama vastaustyyppi. newref1 on esimerkkinä sisämonadimme ST s operaation nostamisesta ulkomonadiimme. throwerror taas on esimerkki uudesta ominaisuudesta jonka tämä muunnin lisäsi sisämonadiimme. import Control.Monad.Error import Control.Monad.ST import Data.STRef type ErrorST s a = ErrorT String (ST s) a newref1 :: a -> ErrorST s (STRef s a) newref1 = lift. newstref suojattu1 :: Double -> Either String Bool suojattu1 x = runst $ runerrort $ do y <- newref1 True if x<0 then throwerror "Ei negatiivisia!" else return $ x>0 Tämän ulkomonadimme suoritusfunktio on runerrorst :: ErrorST s a -> Either String a runerrorst = runst. runerrort kuten suojattu1 osoittaa. Sen tulos on siis joko Left virheilmoitusteksti tai Right vastaus. Tällaisen suoritusfunktion rakenne on vastaava tyyppi toisinpäin: Tyypin N (M a) suoritusfunktio on muotoa runm (runn...) koska N-laskenta tuottaa M-laskennan joka tuottaa vastauksen. Tällaisen suoritusfunktion kirjoittaminen on hyvää ohjelmointityyliä. Valitettavasti runst:n tyypin eksistenssikvantifiointi aiheuttaa sen, ettei Haskell pystykään päättelemään tätä tyyppiä omin voimin. Tämän ongelman voisi kiertää kirjoittamalla operaattorin (.) tälle käyttötilanteelle käsin sen tämänhetkisen tyypin (kvanttoreineen) mutta sivuutamme sen nyt. 123

Tämä monadimuunnin ErrorT laajentaakin aiempaa monadia Maybe :: * -> * joka toteutti yksinkertaiset poikkeukset. Mayben virhetilanne Nothing ei sisällä lisäinformaatiota poikkeuksen syystä. Monadi Either e :: * -> * sisältää tämän lisäinformaation tyyppiä e. Sen jäsenyys voisi siis alkaa instance Monad (Either e) where (Right x) >>= k = k x j@(left _) >>= _ = j return = Right Mayben jäsenyyden pohjalta. Voimme vielä parantaa tyyppiturvallisuutta määrittelemällä kokonaan uuden tyypin joka käyttää monadimuunninta vain sisäisesti. Voimme tehdä sen newtypenä, koska tyypillämme on vain yksi kenttä, eli se muuntimella luomamme monadi. Meidän on liitettävä oma tyyppimme tyyppiluokan Monad jäseneksi. Meidän on liitettävä oma tyyppimme myös tyyppiluokan MonadError jäseneksi, jotta sillä olisi ne muuntimen lisäämät ominaisuudet kuten throwerror. ghc ilmoittaa mitkä instancet vielä tarvitaan. Nämä puuttuvat instancet saadaan puolestaan vaivatta deriving-mekanismilla ja kielilaajennuksella GeneralizedNewtypeDeriving joka monipuolisti sitä newtypeille. {-# LANGUAGE GeneralizedNewtypeDeriving #-} import Control.Monad.Error import Control.Monad.ST import Data.STRef newtype ErrST s a = ErrST{ errst :: ErrorT String (ST s) a } deriving(monad,monaderror String) runerrst = runerrort. errst newref2 :: a -> ErrST s (STRef s a) newref2 = ErrST. lift. newstref suojattu2 x = runst $ runerrst $ 124

do y <- newref2 True if x<0 then throwerror "Ei negatiivisia!" else return $ x>0 Haskell Platformin kirjasto Control.Monad.State tarjoaa erityisen kätevän monadimuuntimen StateT. Se lisää monadiseen koodiin taustaparametrin joka kulkee automaattisesti monadisen suorituksen rinnalla ilman lisäohjelmointia. Ohjelmoija voi siis käsitellä tätä taustaparametria monadisessa koodissaan silloin kun hän itse haluaa, eikä hänen tarvitse muulloin ottaa sitä erikseen huomioon. Sen monadisia operaatioita ovat esimerkiksi get joka lukee taustaparametrin nykyisen arvon put joka korvaa sen nykyisen arvon uudella Sen run-funktioita ovat esimerkiksi runstatet tämä alkuarvo joka suorittaa tämän laskennan tällä taustaparametrin alkuarvolla ja tuottaa parin (vastaus,loppuarvo) evalstatet tämä alkuarvo jonka tulos onkin pelkkä vastaus execstatet tämä alkuarvo jonka tulos onkin pelkkä loppuarvo. Samassa kirjastossa on myös monadi State joka tarjoaa samat palvelut. Se on määritelty lyhenteenä type State s = StateT s Identity eli tämän monadimuuntimen soveltamisena monadiin... Identity joka on siinä mielessä tarpeeton monadi, että se ei sisällä mitään omaa laskentaa. Identity on kuitenkin monadimuuntimissa hyödyllinen monadi, koska tällä tavalla saadaan myös monadi M monadimuuntimen MT sivutuotteena. 7.6 Monadinen jäsennys Jäsennys (parsing) on syötteenä saadun merkkijonon muuntamista sitä vastaavaksi jäsennyspuuksi (tai muuksi semanttiseksi rakenteeksi ) annetun kieliopin mukaisesti. Se on epädeterminististä laskentaa, koska kielioppi voi olla moniselitteinen eli sallia samalle merkkijonolle monta eri jäsennystä ja jäsennyspuuta jäsennettäessä merkkijonoa alusta eteenpäin voi paikallisesti olla useita eri lupaavia tapoja jatkaa jäsennystä, vaikka yksiselitteinen kielioppi sulkisikin lopulta pois kaikki muut kuin yhden niistä. 125

Aiemmin näimme, että tällaisen epädeterministisen laskennan eri vaihtoehdot voi esittää Haskell-listana, jonka jokainen alkio on yhden epädeterminisen laskennan välitilanne. Jäsennyksessä tällainen välitilanne on pari (taimi,loppuosa) jossa taimi on se jäsennyspuun osa, joka on koottu jo käsitellystä merkkijonon alkuosasta loppuosa merkkijonosta, joka on vielä jäsentämättä. Lopputilanne on silloin pari (jäsennyspuu,""). Dr. Seussin the Cat in the Hat sanoisi tämän: A parser for Things is a function from Strings to lists of pairs of Things and Strings. Haskellilla sama tyyppi ilmaistaan type Parser t = String -> [(t,string)]. Epädeterminisyyden lisäksi jäsennys on monadista toisestakin syystä: Jäsennyksessä on luonteva peräkkäisjärjestys jäsennä ensin merkkijonon alkuosa tällä tavalla ja jatka sitten jäljelle jääneen loppuosan jäsentämistä tuolla tavalla. Tätä lähestymistapaa jäsennykseen kutsutaan monadisiksi jäsenninkombinaattoreiksi (monadic parser combinators) koska jokainen yksittäinen jäsennin on monadinen mutkikkaammat jäsentimet kootaan yhdistelemällä yksinkertaisempia jäsentimiä toisiinsa. Voisimmekin ryhtyä itse kehittämään tätä jäsennystapaa kirjoittamalla määritelmät newtype Parser t =... instance Monad Parser where... mutta kun mukaan lisätään vielä jäsennysvirheiden käsittely Eitherilla kuten edellä, niin on vaivattomampaa ryhtyä käyttämään valmista työkalua. Haskell Platform sisältää raskaan sarjan jäsennystehtäviin erilliset työkalut Happy joka on Haskell-vastine UNIXin yacc- ja GNUn bison-työkalulle. Ne ovat metakääntäjiä (metacompiler) jotka tekevät annetulle kontekstittomalle (LA)LR-kieliopille kokoavan (bottom-up) jäsentäjän. Esimerkiksi ghc itse on toteutettu sen avulla. Alex joka on Haskell-vastine UNIXin lex- ja GNUn flex-työkalulle. Raskaan sarjan jäsentäminen jaetaan kahteen vaiheeseen: 126

selaukseen jossa syötemerkkijono kootaan suuremmiksi yksiköiksi, kuten ohjelmointikielissä varatuiksi sanoiksi, muuttujannimiksi,... Tässä vaiheessa poistetaan myös sellaiset osat joiden ei haluta näkyvän enää jäsennysvaiheessa, eli välilyönnit, rivinvaihdot, kommentit,... jäsennykseen joka jäsentää näin saadun suurempien yksikköjen listan. Nämä työkalut tuottavat selaajan, kun annetaan jäsennettävän kielen perusalkioiden kuvaus säännöllisinä lausekkeina. Niiden sijaan tutustummme keskiraskaan sarjan jäsennystehtäville sopivaan Haskell Platformin jäsenninkombinaattorikirjastoon Text.Parsec. (O Sullivan et al., 2008, luku 16) Jäsenninkombinaattorit ovat rekursiivisesti etenevää (recursive descent) jäsennystä. Rekursiivisesti etenevä jäsennys on osittavaa (top-down) ja se soveltuu LLkieliopeille. Emme kytke Parseciin erillistä selaajaa, vaan varaudumme itse kieliopissamme välilyönteihin yms. Haskell Platfrom sisältää myös kirjaston Data.Attoparsec joka käyttää samaa lähestymistapaa jäsennykseen tuottaa tehokkaampia jäsentäjiä vaatii enemmän ohjelmointia kuin Parsec. Muistutetaan mieliin kontekstittoman kieliopin idea: Se koostuu säännöistä muotoa välikesymboli vaihtoehto vaihtoehto vaihtoehto. vaihtoehto joka sanoo, että tällä välikesymbolilla on (vain) nämä vaihtoehdot. Jokainen vaihtoehto on puolestaan (äärellinen) jono muotoa symboli symboli symboli... symboli jonka jokainen symboli on joko välike- tai päätesymboli. Jokin näistä välikesymboleista on erityinen alkusymboli. Sitä on tapana merkitä S. Tällaisen kieliopin välikesymbolin A tuottamat johdokset määritellään induktiivisesti (eli kielioppisääntöjen pienimpänä kiintopisteenä): A itse on johdos. 127

Jos symbolijono muotoa αbγ on välikesymbolin A tuottama johdos, ja kieliopissa on sääntö muotoa B... β... niin myös symbolijono αβγ on välikesymbolin A tuottama johdos. Välikesymbolin A tuottama kieli koostuu niistä päätesymbolijonoista jotka ovat välikesymbolin A tuottamia johdoksia. Tällaisen kieliopin tuottama kieli on sen alkusymbolin S tuottama kieli. Tällaisen kieliopin tunnistusongelma on Tässä on päätesymbolijono δ. Kuuluuko se kielioppisi tuottamaan kieleen? jäsennysongelma on Tässä on päätesymbolijono δ. Voitko jäljittää sen tavan, miten alkusymbolisi S tuotti tämän johdoksen δ? Tämä tapa voidaan esittää jäsennyspuuna. Nämä jäsennyspuut voidaan esittää luontevasti algebrallisilla tietotyypeillä. Useimmiten haluammekin muuntaa syötemerkkijonon δ sitä vastaavaksi algebrallisen tietotyypin arvoksi. Toisin sanoen, haluammekin toteuttaa saman, jonka metodi read :: (Read a) => String -> a tekee, mutta omalla (eikä Haskellin) kieliopilla algebrallisen tietotyypin a arvojen tekstiesitykselle. Rekursiivisesti etenevän jäsentämisen perusidea on: Jokaiselle välike- ja päätesymbolille ohjelmoidaan sen oma rekursiivinen aliohjelma. Välikesymbolin A aliohjelman tehtävänä on lukea jäljellä olevan syötteen alusta jokin sen tuottama päätesymboleista koostuva johdos. Päätesymbolin aliohjelman tehtävänä on lukea jäljellä olevan syötteen alusta sen tekstiesitys. Nämä aliohjelmat kutsuvat rekursiivisesti toisiaan siten kuin kielioppi esittää. Tavallisesti sen ohjelmointi ennustavana (predictive) vaatii, että kielioppi täyttää LL(k)-ehdon, jonka mukaan tällainen välikesymbolin A aliohjelma voi valita sen säännössä muotoa A α 1 α 2 α 3... α p juuri oikean vaihtoehdon α i tutkimalla vain seuraavaa k syötemerkkiä tavallisesti tämä kurkistus (lookahead) k = 1. Haskellin epädeterminismi- eli listamonadi syötemerkkijonon käsittely laiskana merkkilistana kuten getcontents do-syntaksi 128

osoittavat toisen tavan valita vaihtoehto α i kuin LL(k): Seurataan epädeterministisesti eri vaihtoehtoja α 1, α 2, α 3,...,α p kunnes joku niistä onnistuu. Tämä epädeterminismi ja siihen liittyvä syötemerkkijono käsittely piilotetaan jäsennysmonadin sisälle. Silloin jokainen vaihtoehto α j voidaan kirjoittaa jäsennysmonadisena do-lausekkeena...... ja koko välikesymbolin A aliohjelma voidaan kirjoittaa yhdistämällä ne toisiinsa sopivalla jäsenninkombinaattorilla joka toteuttaa pystyviivalla esitetyn epädeterministisen valintaoperaation tai. Esimerkkinä olkoon tavalliset aritmeettiset lausekkeet S SAB B B BCD D D (S) e x A + - C * / jossa e on liukulukuvakio x on muuttujannimi. Niiden rakennetta ei tavallisesti kuvata kieliopissa, koska säännölliset lausekkeet riittäisivät siihen ne käsitellään tavallisesti selaajassa eikä jäsentäjässä jäsennetään ne Haskellin selaajalla, jonka Parsec tarjoaa valmiina. Kirjoitamme systemaattisesti jokaiselle välikesymbolille X jäsennysfunktion nimeltä parsex. sellaisen kutsut muodossa x <- parsex jossa x on sen palauttama semanttinen tulos. välikesymbolin X vaihtoehdot muodossa parsex = do a <- parsea b <- parseb c <- parsec return $ sem a b c... X ABC... semantiikkafunktiot jotka kokoavat jäsennyspuun rekursiokutsujen antamista alipuista. Emme vielä tässä vaiheessa rakenna jäsennyspuita, vaan palautamme aina arvon (). 129

import Text.Parsec -- Tässä otetaan käyttöön Parsecin Haskell-selaaja. import qualified Text.Parsec.Token as Token import Text.Parsec.Language(haskellDef) lexer = Token.makeTokenParser haskelldef -- "A parser for things..." type Parser thing = Parsec String () thing -- Tässä on jäsennyksen pääohjelma. Se kutsuu alkusymbolin -- jäsennysfunktiota. (Tyhjää) merkkijonoparametria -- käytetään virheilmoituksiin. jasenna = parse (do s <- parses eof -- Jäsentää syötteen lopun. return s) "" parses :: Parser () parses = do s <- parses a <- parsea b <- parseb return () do b <- parseb return () parsea = do char + return () do char - return () parseb = do b <- parseb c <- parsec d <- parsed return () do d <- parsed return () parsec = do char * return () do char / 130

return () parsed = do char ( s <- parses char ) return () do e <- Token.float lexer return () do x <- Token.identifier lexer return () Kielioppeihin liitetään kolmenlaisia attribuutteja (attribute) ja niiden muodostussääntöjä. Syntesoidut (synthesized) attribuutit muodostetaan jäsennyspuussa alhaalta ylöspäin lehdistä juureen S päin. Monadisessa jäsennyksessä nämä ovat niitä, jotka palautetaan jäsennysfunktioista parsex returnilla rekursiokutsuista. Vielä nyt ne ovat esimerkissämme() mutta täydennämme ne myöhemmin. Perityt (inherited) attribuutit muodostetaan jäsennyspuussa ylhäältä alaspäin juuresta S lehtiin päin. Monadisessa jäsennyksessä nämä esitetään jäsennysfunktioiden parsex parametreina. Emme tarvitse niitä esimerkissämme. Muut attribuutit ovat taustainformaatiota. Parsecissa ne voidaan esittää kuten monadissa Control.Monad.State. Emme tarvitse niitä esimerkissämme, joten määrittelemme niille tyypin () omassa Parserissamme. Tämä koodi kääntyy mutta jumittuu ikuiseen silmukkaan: parses (ja parseb) kutsuu itseään rekursiivisesti lukematta syötettä. Tämä vasen (left) rekursio on ongelmallista rekursiivisesti enenevälle jäsennykselle. Sitä tarvitaan vasemmalle assosioiville operaattoreille kuten - (ja / ) jolla x y z on (x y) z eikä x (y z). Funktion parses pitäisi arvata sokkona epädeterministisesti oikein, montako vähennyslaskua - syötteessä on tulossa eikä sen listatoteutus siihen pysty, koska se on syvyyssuuntaista etsintää. Tämä ongelma ratkaistaan tavallisesti muuttamalla kielioppia niin, että vasen rekursio poistuu. Parsecissa tämä kieliopin muuttaminen on ohjelmoitu monadisena kontrollirakenteena 131

chainl1 :: Parser a -> Parser (a -> a -> a) -> Parser a jolla ketju BABABA... B voidaan kirjoittaa chainl1 parseb parsea joka välttää jumittumisen. import Text.Parsec import Text.Parsec.Combinator -- Tässä otetaan käyttöön Parsecin Haskell-selaaja. import qualified Text.Parsec.Token as Token import Text.Parsec.Language(haskellDef) lexer = Token.makeTokenParser haskelldef -- "A parser for things..." type Parser thing = Parsec String () thing -- Tässä on jäsennyksen pääohjelma. Se kutsuu alkusymbolin -- jäsennysfunktiota. (Tyhjää) merkkijonoparametria -- käytetään virheilmoituksissa. jasenna = parse (do s <- parses eof return s) "" parses :: Parser () parses = chainl1 parseb parsea parsea = do char + return $ \ () () -> () do char - return $ \ () () -> () parseb = chainl1 parsed parsec parsec = do char * return $ \ () () -> () do char / return $ \ () () -> () parsed = Token.whiteSpace lexer >> 132

(do s <- Token.parens lexer parses return () (try $ do e <- Token.float lexer return ()) do e <- Token.natural lexer return () do x <- Token.identifier lexer return ()) Lisätään samalla välilyöntien käsittely: Poistetaan välilyönnit yms. Token.whiteSpace ennen välikesymbolia D sen jäsennysfunktiossa parsed. Sen Token-funktiot poistavat ne sen jälkeen. Tämän vuoksi siirrytään käsittelemään myös (S) Token-funktiolla. Token.float jäsentää Haskell-liukulukuvakion, jossa siis pitää olla desimaalipiste. Sallitaan myös (etumerkitön) kokonaislukuvakio Token.natural jotta sitä ei tarvita. Parsecin ei olekaan sama kuin kielioppien. Sen sijaan eka toka tarkoittaakin, että tokaa kokeillaan vain jos eka epäonnistui lukematta yhtään syötettä. Tässä siis yritetään ensin lukea aito liukulukuvakio, mutta jos se epäonnistuu (desimaalipisteen puuttumisen vuoksi) niin sitten yritetään jäsentää samat numeromerkit kokonaislukuvakiona. Jos tarvitaan oikeaa (kuten nyt) niin (try eka) toka muuntaa ekan sellaiseksi jäsentimeksi, joka epäonnistuessaan palauttaa lukemansa syötteen takaisin tokan luettavaksi. Näin tämä Parsecin try sallii LL( )-jäsennyksen, eli sellaisen rekursiivisen jäsennyksen, jossa ei olekaan kiinteää kurkistusylärajaa k. Tämä ero johtuu siitä, että luonnollinen kieli on moniselitteistä (eli saman syötteen voi jäsentää monella eri tavalla) ja mallintaa sitä formaalisti määritellyt kielet (kuten ohjelmointikielet) suunnitellaan yksiselitteisiksi. Tämä ero heijastuu jäsennysvirheiden käsittelyyn: luonnollisessa kielessä pitää silloin kokeilla jotakin toista jäsennystapaa ja tekee niin formaalisti määritellyssä kielessä pitääkin pysähtyä virheilmoitukseen ja tekee niin ohittamalla ne toiset jäsennystavat, kuten monadit Maybe ja Either tekivät. 133

Näin välttää turhan ja pahimmassa tapauksessa eksponentiaalisen (!) toisten jäsennystapojen kokeilun. Siis (try eka) toka sanoo, että jos ekassa tulee jäsennysvirhe, niin yritä tokaa vaikka tavallinen reaktio olisikin pysäyttää jäsennys siihen virheeseen. Nyt kun syntaksi on kunnossa siirrymme semantiikkaan. Jäsennyspuun tyyppi voidaan määritellä systemaattisesti määrittelemällä jokaiselle välikesymbolille X oma datatyyppinsä PuuX välikesymbolin X vaihtoehdot muodossa eli X ABC DEF... PuuX = KonstruktoriX1 PuuA PuuB PuuC KonstruktoriX2 PuuD PuuE PuuF... jokaiselle sen vaihtoehdolle oma konstruktori jokaisen konstruktorin kentät samassa järjestyksessä kuin sen vaihtoehdossa symbolit. Tämän taustalla on tietenkin se, että sekä kieliopin että data-määrittelyn on tai vaihdoehdon symboleilla että konstruktorin kentillä on keskinäinen järjestys. Näin sammme systemaattisesti tarkan data-määrittelyn alkuperäisen kielioppimme jäsennyspuille. Koska poistimme siitä vasemman rekursion monadisella kontrollirakenteella chainl1, jäsentimemme käyttääkin implisiittisesti siten muutettua kielioppia, vaikka emme konstruoineetkaan sitä eksplisiittisesti. Siten meidän on muokattava kontrollirakenteen chainl1 käyttöparametreja siten, että ne luovat alkuperäisen kielioppimme mukaisen jäsennyspuun. Se ei kuitenkaan ole työlästä algbrallisilla tietotyypeillä. Tämä on kuitenkin vähemmän mutkikasta kuin muodostaa muutettu kielioppi eksplisiittisesti ja tuottaa sen mukaisia jäsennyspuita, koska haluttu semantiikka on tavallisesti suunniteltu alkuperäisen kieliopin mukaisesti. Tietojenkäsittely- ja formaalissa kielitieteessä erotellaan konkreettinen syntaksi joka kuvaa merkkijonojen rakenteen. abstrakti syntaksi joka kuvaa sen semanttisen puurakenteen, jota merkkijono esittää. Abstrakti syntaksi voidaan nähdä tämän kaltaisena data-määritelmänä jossa ei ole jäänteitä konkreettisesta syntaksista. Jäsennyspuissa on vielä sen jäänteitä. 134

import Text.Parsec import Text.Parsec.Combinator -- Tässä otetaan käyttöön Parsecin Haskell-selaaja. import qualified Text.Parsec.Token as Token import Text.Parsec.Language(haskellDef) lexer = Token.makeTokenParser haskelldef -- "A parser for things..." type Parser thing = Parsec String () thing -- Jäsennyspuutyypit. data PuuS = VaihtoehtoS1 PuuS PuuA PuuB VaihtoehtoS2 PuuB deriving (Show) data PuuA = VaihtoehtoA1 VaihtoehtoA2 deriving (Show) data PuuB = VaihtoehtoB1 PuuB PuuC PuuD VaihtoehtoB2 PuuD deriving (Show) data PuuC = VaihtoehtoC1 VaihtoehtoC2 deriving (Show) data PuuD = VaihtoehtoD1 PuuS VaihtoehtoD2 Double VaihtoehtoD3 String deriving (Show) -- Tässä on jäsennyksen pääohjelma. Se kutsuu alkusymbolin -- jäsennysfunktiota. (Tyhjää) merkkijonoparametria -- käytetään virheilmoituksissa. jasenna = parse (do s <- parses eof return s) "" parses :: Parser PuuS parses = chainl1 (fmap VaihtoehtoS2 parseb) parsea parsea :: Parser (PuuS -> PuuS -> PuuS) parsea = do char + return $ \ vasen (VaihtoehtoS2 oikea) -> VaihtoehtoS1 vasen VaihtoehtoA1 oikea do char - return $ \ vasen (VaihtoehtoS2 oikea) -> 135

VaihtoehtoS1 vasen VaihtoehtoA2 oikea parseb :: Parser (PuuB) parseb = chainl1 (fmap VaihtoehtoB2 parsed) parsec parsec :: Parser (PuuB -> PuuB -> PuuB) parsec = do char * return $ \ vasen (VaihtoehtoB2 oikea) -> VaihtoehtoB1 vasen VaihtoehtoC1 oikea do char / return $ \ vasen (VaihtoehtoB2 oikea) -> VaihtoehtoB1 vasen VaihtoehtoC2 oikea parsed :: Parser PuuD parsed = Token.whiteSpace lexer >> (do s <- Token.parens lexer parses return $ VaihtoehtoD1 s (try $ do e <- Token.float lexer return $ VaihtoehtoD2 e) do e <- Token.natural lexer return $ VaihtoehtoD2 $ frominteger e do x <- Token.identifier lexer return $ VaihtoehtoD3 x) Semanttiset rakenteet eivät aina ole jäsennyspuita tai abstraktia syntaksia. Semanttinen rakenne valitaan halutun käyttötarkoituksen mukaan. Valitaan käyttötarkoitukseksemme laatia semanttisena rakenteena merkkijonosta funktio tyyppiä joka [(String,Double)] -> Either String Double ottaa sisään syötelistan pareja (x,d) jossa x on muuttujannimi ja d sitä vastaava liukulukuarvo antaa vastauksenaan merkkijonon kuvaaman lausekkeen arvon kun sen jokaiselle muuttujalle x annetaan sitä vastaava arvo d tai virheilmoituksen jos syötelistasta puuttuikin jokin sellainen muuttujannimi x joka esiintyy lausekkeessa. Nyt meillä on kaksi monadia sisäkkäin: Ulompana on Parsecin jäsennysmonadi. Sisempänä on tämä semanttinen funktio, joka on Either String -virheenkäsittelymonadissa. 136

import Text.Parsec import Text.Parsec.Combinator import Control.Monad.Error -- Tässä otetaan käyttöön Parsecin Haskell-selaaja. import qualified Text.Parsec.Token as Token import Text.Parsec.Language(haskellDef) lexer = Token.makeTokenParser haskelldef -- "A parser for things..." type Parser thing = Parsec String () thing -- Käännöksen tulostyyppi. type LookupFn = [(String,Double)] -> Either String Double -- Tässä on jäsennyksen pääohjelma. Se kutsuu alkusymbolin -- jäsennysfunktiota. (Tyhjää) merkkijonoparametria -- käytetään virheilmoituksissa. jasenna = parse (do s <- parses eof return s) "" parses :: Parser LookupFn parses = chainl1 parseb parsea parsea :: Parser (LookupFn -> LookupFn -> LookupFn) parsea = do char + return $ binop (+) do char - return $ binop (-) binop :: (Double -> Double -> Double) -> LookupFn -> LookupFn -> LookupFn binop op p q xs = do p <- p xs q <- q xs return $ p op q parseb :: Parser LookupFn parseb = chainl1 parsed parsec parsec :: Parser (LookupFn -> LookupFn -> LookupFn) 137

parsec = do char * return $ binop (*) do char / return $ binop (/) parsed :: Parser LookupFn parsed = Token.whiteSpace lexer >> (Token.parens lexer parses (try $ do e <- Token.float lexer return $ const $ return e) do e <- Token.natural lexer return $ const $ return $ frominteger e do x <- Token.identifier lexer return $ maybe (throwerror $ "Muuttujalta " ++ x ++ " puuttuu arvo!") return. lookup x) kaanna :: (Monad m) => String -> m LookupFn kaanna = either (error. show) return. jasenna Parsec on esimerkki sovelluskohtaisesta kielestä (Domain Specific Language, DSL). DSL-ideassa lähestytään yleistä ohjelmointiongelmaa niin, että 1 ensin suunnitellaan ja toteutetaan juuri tällaisiin ohjelmointiongelmiin soveltuva pieni ohjelmointikieli 2 sitten käytetään tätä pientä kieltä kun ratkotaan tämän ohjelmointiongelman tapauksia. Parsecissa yleinen ohjelmointiongelma on jäsennys. 1 Parsecissa on rekursiivisesti etenevän jäsentämisen primitiivit sekä välineet niiden yhdistelemiseksi toisiinsa. 2 Näillä Parsecin primitiiveillä on vaivattomampaa kirjoittaa annetulle kieliopille rekursiivisesti etenevä jäsentäjä kuin perus-haskellilla. Haskell on hyvä alusta DSL-ohjelmoinnille: Toteutettava DSL voi lainata Haskellin funktiokäsitteen itselleen, koska laiskuutensa nojalla Haskell vain välittää parametreja funktioiden välillä laskematta niitä DSL voi itse päättää milloin se ne laskee. Laiskuutensa vuoksi Haskellilla on myös vaivatonta määritellä uusia DSLkontrollirakenteita kuten Parsecin. Muissa ohjelmointikielissä käytettäisiin makroja tms. 138

DSL vodaan määritellä monadisena, jolloin sen peräkkäissuoritus voidaan määritellä sen metodina (>>=) kirjoittaa do-syntaksilla. Esimerkiksi Parsec määritteli oman peräkkäissuorituksensa tarkoittavan että jäsennä syötettä eteenpäin. 8 Abstraktit tietotyypit Haskellissa on yksinkertainen modulijärjestelmä, joka on tarkoitettu abstraktien (abstract) tietotyyppien määrittelemiseen. Yleisesti tietotyyppi on abstrakti, jos sen toteutus on piilotettu sitä käyttävältä ohjelmakoodilta, jotta koodin täytyy käyttää tätä tyyppiä vain siten kuin sen ohjelmoija on tarkoittanut. Algebrallisilla tietotyypeillä se tarkoittaa tietotyyppiä, jonka konstruktorit on piilotettu sitä käyttävältä ohjelmakoodilta, koska silloin koodi ei voi luoda eikä tutkia sen arvoja omin päin vaan ainoastaan siten kuin sen ohjelmoija on tarkoittanut. Luvussa 6.2 kuvatut eksistentiaaliset tyypit ovat tällaisia abstrakteja tyyppejä, koska niiden arvoa pystyi käsittelemään vain tyyppiluokkana annetun rajapinnan kautta (jos sellainen annettiin). Ohjelmointikielten teoriassa modulit voidaankin nähdä tapana määritellä eksistentiaalisia tyyppejä. Funktionaalisessa ML-kieliperheessä kuten Standard ML (Milner et al., 1997, luvut 3 ja 5) ja (O)Caml http://caml.inria.fr/ on kehitetty paljon Haskellia monipuolisempia modulijärjestelmiä. Tosin niissä ei vuorostaan ole tyyppiluokkia. Haskellin moduleilla ilmaistaan, mitkä ohjelman tässä lähdekooditiedostossa määritellyistä funktioista, tyypeistä ja tyyppiluokista ovat paikallisia eli käytettävissä vain tässä samassa lähdekooditiedostossa julkisia eli käytettävissä kaikissa muissakin lähdekooditiedostoissa, jotka ottavat ne käyttöön importilla. Jos Haskell-lähdekooditiedosto on oma modulinsa, niin silloin sen rakenne on: module Hierarkinen.Modulin.Nimi viennit where tuonnit määritelmät ghci olettaa näin otsikoidun hierarkisen modulin löytyvän tiedostosta nimeltä Linuxissa Hierarkinen/Modulin/Nimi.hs Windowsissa hierarkinen\modulin\nimi.hs jonka polku alkaa 139

nykyisestä työhakemistosta josta ohjelmoija voi siis aloittaa tälle nyt tekeillä olevalle ohjelmalleen sen oman moduli- ja tiedostohierarkian asennuksen yhteydessä määritellystä pakkaushakemistosta josta alkaen se löytää vakiokirjastonsa kuten Data.Word jne. muista hakemistoista joita voi antaa optioina joko komentorivillä tai lähdekooditiedoston erikoiskommentissa {-# OPTIONS_GHC... #-} Ainakin Linuxissa näitä(kin) optioita voi asettaa myös 8.1 Tuonnit työhakemistokohtaisessa alustustiedostossa./.ghci tai käyttäjäkohtaisessa alustustiedostossa /.ghci. Modulin tuonnit koostuvat tuontilauseista jollaisen perusmuoto on import [qualified] Toinen.Hierarkinen [as Lyhenne] johon tutustuimmekin jo luvussa 4.6. Jos haluaakin tuoda modulin tekemistä määritelmistä vain osan, niin ne voi antaa monikkona (eli suluissa (...) pilkuin, eroteltuna) koko importin lopussa. Tai jos haluaakin tuoda modulin tekemistä määritelmistä muut kuin tässä monikossa mainitut, niin tämän monikon eteen voi kirjoittaa hiding. 8.2 Viennit Modulin viennit on sekin monikko, jossa kerrotaan mitä määritelmiään tämä moduli julkistaa ja miten. Jos tämä monikko viennit puuttuu, niin tämä moduli julkistaa kaikki määritelmänsä (paitsi importit). Yksinkertaisin julkistettava määritelmä on jonkin muuttujan nimi. Toinen julkistettava määritelmä on module Toinen.Hierarkinen jossa tässä samassa modulissa on myös import Toinen.Hierarkinen ja joka myös julkistaa sen modulin Toinen.Hierarkinen jonka se itse on ottanut käyttöön. Esimerkiksi moduli Data.Array julkistaa myös modulin Data.Ix, koska jos jokin moduli käyttää taulukoita, niin se käyttänee myös taulukkoindeksien operaatioita. Tällöin taulukkoja käyttävä modulissa riittää pelkkä import Data.Array 140

Kolmas julkistettava määritelmä on type-synonyymi. Se julkistetaan kirjoittamalla sen nimi monikkoon viennit. Neljäs julkistettava määritelmä on tyyppiluokka. Se voidaan julkistetaan kolmella eri tavalla: Pelkkä TyyppiLuokanNimi julkaisee sen siten, että muut modulit voivat käyttää sitä rajoitteissaan (...) => mutta eivät voi liittää omia tyyppejään sen uusiksi jäseniksi koska tämä moduli ei julkistanutkaan sen metodeja. Sen perään voi kirjoittaa monikkona niiden sen metodien nimet, jotka tämä moduli haluaa julkistaa. Silloin muut modulit voivat liittää omia tyyppejään sen jäseniksi määrittelemällä nämä metodit niiden instanceissa. Tämän monikon erikoismuotona on(..) joka tarkoittaa kaikki sen metodit. Viides julkistettava määritelmä on data-tyyppi. Sekin voidaan julkistaa vastaavasti kolmella eri tavalla: Pelkkä TyypinNimi julkistaa sen kokonaan abstraktina tyyppinä, jota muut modulit voivat käyttää omissa tyypeissään ::... mutta eivät voi käyttää koodissaan hahmojen tai lausekkeiden kautta koska tämä moduli ei julkistanutkaan sen konstruktoreja. Sen perään voi kirjoittaa monikkona niiden sen konstruktoreiden ja kenttien nimet, jotka tämä moduli haluaa julkistaa. Silloin muut modulit voivat käyttää niitä koodissaan, jolloin TyypinNimi onkin vain osittain abstrakti. Tämän monikon erikoismuotona on (..) joka tarkoittaa kaikki sen konstruktorit ja kentännimet. Silloin myös TyypinNimi on muissakin moduleissa täysin vapaasti käytettävissä samoin kuin niiden määrittelemät tyypit. Modulissa voi olla määritelmä muuttuja :: Tyyppi jossa moduli julkaisee tämän muuttujan mutta ei julkaise jotakin sen Tyyppiin sisältyvää itse määrittelemäänsä tyyppiä tai tyyppiluokkaa. Koska muissa moduleissa on voitava käyttää tätä julkaistua muuttujaa, niin myös tämä julkistamaton tyyppi tai tyyppiluokat julkistetaan, mutta vain tyypinpäättelyä varten, eli sellaisena etteivät muut modulit voikaan käyttää sitä omissa tyypeissään. Esimerkkinä tehdään luvun 4.4.7 pinosta abstrakti tietotyyppi. 141

module Stack(Stack,emptyStack,push,pop) where newtype Stack t = IntoStack { fromstack :: [t] } deriving (Show) emptystack :: Stack t emptystack = IntoStack [] push :: t -> Stack t -> Stack t push x = IntoStack. (x :). fromstack pop :: u -> (t -> Stack t -> u) -> Stack t -> u pop e _ (IntoStack []) = e pop _ f (IntoStack (x:xs)) = f x (IntoStack xs) 8.3 Päämoduli Koko ohjelman päämoduli on poikkeus: Sen rakenne on module Main(main) where... main :: IO () main =... Sen tiedostonimi saa olla mikä tahansa ohjelma.hs. Sen kääntäminen komennolla ghc ohjelma.hs tuottaa siitä suorituskelpoisen ohjelmatiedoston nimeltään Linuxissa ohjelma Windowsissa ohjelma.exe samaan hakemistoon. Tämä main on standardoitu nimi Haskellin pääohjelmalle, joka suoritetaan ajamalla tämä ohjelmatiedosto. Pääohjelmaa ei tarvitse kirjoittaa omaksi modulikseen Main. Riittää määritellä tämä main :: IO () lähdekooditiedostossa ohjelma.hs. 142

9 Curryn ja Howardin vastaavuudesta Curryn ja Howardin vastaavuus (the Curry-Howard correspondence) (Sørensen and Urzyczyn, 2006) on symmetria formaalin loogisen todistusteorian ja funktionaalisen ohjelmoinnin välillä. Tietojenkäsittelyteoriassa se on inspiroinut esimerkiksi ohjelmointikielten tyyppiteoriaa ohjelmien verifiointia formaalia ohjelmankehitystä. Kyseessä on jälleen sama loogikko Haskell B. Curry sekä hänen oppilaansa William A. Howard. Tässä on sen perusintuitio. Otamme käyttöön merkintöjä todistusteoriasta: Merkintä nämä = tuo luetaan, että näistä oletuksista voidaan todistaa tuo johtopäätös. Nämä oletukset ja johtopäätökset ovat nyt muotoa eli tämä λ-lauseke on tuota tyyppiä. Esitämme merkinnällä sääntöjä tämän = käytölle. Perussääntö on lauseke :: tyyppi jos olemme osoittaneet kaikki nämä niin voimme osoittaa myös tämän e :: t,... = e :: t eli jos jokin kuuluu jo oletuksiin, niin se voidaan triviaalisti todistaa niistä. Käytämme yksinkertaista tyypitettyä λ-laskentaa, jonka tyyppirakenne on: Tulotyyppi (t,u) jolla on konstruktori (x,y) sekä kirjastofunktiot fst ja snd. Funktiotyyppi t -> u. Summatyyppi Either t u jolla on konstruktorit Left ja Right sekä kirjastofunktio either. Käytämme näitä kirjastofunktioita case-hahmonsovituksen sijasta. Kirjoitamme merkinnöillämme tämän kielen tyypityssääntöjä: 143

Tulotyypin konstruktorin tyypityssääntö on Γ = x :: t Γ = y :: u Γ = (x,y) :: (t,u) (17) ja sen kirjastofunktioiden tyypityssäännöt ovat Γ = z :: (t,u) Γ = fst z :: t ja Γ = z :: (t,u) Γ = snd z :: u. (18) Funktiotyypin konstruktorin λ tyypityssääntö on x :: t, Γ = e :: u Γ = λx.e :: t -> u (19) ja funktionkutsun tyypityssääntö on Γ = f :: t -> u Γ = f a :: u. Γ = a :: t (20) Summatyypin konstruktoreiden tyypityssäännöt ovat Γ = z :: t Γ = Left z :: Either t u ja Γ = z :: u Γ = Right z :: Either t u sekä sen kirjastofunktion tyypityssääntö on Γ = f :: t -> v Γ = g :: u -> v Γ = either f g :: Either t u -> v. (21) (22) (23) Ohjelmointikielten tyyppijärjestelmien teoriassa käytetään tällaisia sääntöjä, koska niillä voidaan spesifioida, mitä tyyppijärjestelmän toteutusten pitää päätellä ohjelmakoodista niitä tutkimalla loogisessa todistusteoriassa kehitetyin välinein voidaan analysoida tyyppijärjestelmän ominaisuuksia. Unohdetaan hetkeksi näistä säännöistä kaikki osat lauseke ::. Tulotyypin konstruktorin tyypityssääntö (17) pelkistyy muotoon = t = u = (t,u) ja sen kirjastofunktioiden tyypityssäännöt (18) muotoihin = (t,u) = t ja = (t,u) = u. Mutta nämähän ovat samat kuin konjunktion t u käyttösäännöt lauselogiikan todistuksissa! 144

Curryn ja Howardin vastaavuuden ensimmäinen osa onkin että lauselogiikan kaavat ovat λ-laskennan tyyppejä ( formulæ as types ) tässä mielessä. Ohjelmoinnissa tyyppi (t,u) onkin sellainen, jonka arvoissa on 1. kenttä tyyppiä t ja 2. kenttä tyyppiä u. Tästä syystä Haskell käyttää monikoita esimerkiksi rajoitteissaan(...)=>. Funktiotyypin tyypityssääntö (19) pelkistyy muotoon t, Γ = u Γ = t -> u ja tyypityssääntö (20) pelkistyy muotoon (24) Γ = t -> u Γ = u Γ = t jotka ovat puolestaan implikaation t u käyttösäännöt. Erityisesti sääntö (25) on päättelyaskel Modus Ponens. Summatyypin konstruktoreiden tyypityssäännöt (21) ja (22) pelkistyvät muotoihin = t = u ja = Either t u = Either t u ja sen kirjastofunktion tyypityssääntö (23) pelkistyy muotoon = t -> v = u -> v = Either t u -> v (25) (26) jotka ovat puolestaan disjunktion t u käyttösäännöt. Säännön (24) nojalla sääntö (26) kertoo, miten t u toimii ollessaan oletuksena. Ohjelmoinnissa tyyppi Either t u onkin sellainen, jonka arvossa on joko kenttä tyyppiä t tai kenttä tyyppiä u, ja tiedämme kumpi niistä. Tämä on kuitenkin konstruktiivinen eli intuitionistinen logiikka, koska t u voidaan osoittaa vain jos joko t voidaan osoittaa tai u voidaan osoittaa. Tällaisessa logiikassa esimerkiksi väitettä Marsissa joko on elämää tai siellä ei ole elämää ei voikaan (vielä) osoittaa, koska emme ole osoittaneet kumpaakaan näistä kahdesta mahdollisuudesta. Tutummassa klassisessa logiikassa ei sanotakaan voidaan osoittaa vaan on totta. Sellaisessa logiikassa Mars-esimerkkiväitteemme on totta, koska joko Marsissa on elämää tai siellä ei ole elämää on totta, vaikka emme tiedäkään (vielä) kumpi niistä on totta. Palautetaan sitten sääntöihin niistä unohdetut osat lauseke ::. Ne voidaan nähdä esityksinä vastaavan todistuksen rakenteelle, koska lausekkeen uloimmainen konstruktori tai funktionkutsu identifioi sen säännön, jota käytetään seuraavaksi. Curryn ja Howardin vastaavuuden toinen osa onkin, että lauselogiikan todistukset ovat λ-laskennan termejä eli laajemmin tulkittuna funktionaalisia ohjelmia ( proofs as programs ) tässä mielessä. 145

Tässä valossa tyypitetty β-reduktioaskel (β) eli funktionaalisen ohjelman suorituksen perusaskel (λx.f :: t -> u) (g :: t) f[x g] :: u on todistuksen sieventämistä poistamalla siitä jokin Modus Ponens -päättelyaskel (25) siten, että väitteen t todistus g sijoitetaan kaikkialle missä oletusta x käytetään. Voidaan määritellä kolme ongelmaa: Tyypintarkistus: Annetaan lauseke ja tyyppi, ja pitää selvittää päteekö niille lauseke :: tyyppi vaiko ei. Tämä on se ongelma, jota kaikki staattisesti tyypitetyt ohjelmointikielet ratkovat käännösaikana. Tutkimuksessa pyritään kehittämään sellaisia ilmaisuvoimaisia tyyppijärjestelmiä, jotka rajoittaisivat mahdollisimman vähän ohjelmoijan ilmaisunvapautta. Tyypinpäättely: Annetaan vain lauseke, ja pitää löytää sellainen tyyppi jolla lauseke :: tyyppi pätee (tai osoittaa, ettei sellaista tyyppiä ole). Tämä on se ongelma, jota tyypinpäättelevät ohjelmointikielet kuten Haskell ratkovat käännösaikana. Tutkimuksessa pyritään kehittämään sellaisia päättelymekanismeja, joilla tällaiset ilmaisuvoimaisemmat tyyppijärjestelmät olisivat ohjelmoijalle näkymättömiä. Tyyppiohjattu ohjelmointi: Annetaan vain tyyppi, ja pitää löytää sellainen lauseke jolla lauseke :: tyyppi pätee (tai osoittaa, ettei sellaista lauseketta ole). Curryn ja Howardin vastaavuuden näkökulmasta tämä ongelma on: Tässä on spesifikaatio logiikalla ilmaistuna. Kehitä sen mukainen ohjelma! Käytimme kurssilla perusversiota tästä lähestymistavasta, kun lähdimme ohjelmoimaan funktiota 1 kirjoittamalla ensin sille tyypin 2 sitten valitsemalla jonkin parametrin ja haarautumalla koodissa sen tyypin konstruktoreiden mukaan. Kysymystä Kuuluuko tähän tyyppiin yhtään lauseketta? kutsutaan sen asutusongelmaksi (type inhabitation problem). Ohjelmointinäkökulmasta se on Onko tämä spesifikaatio mahdollinen vai mahdoton toteuttaa? On kehitetty logiikkoja, joiden tietokonetoteutukset pystyvät generoimaan todistuksesta sitä vastaavan funktionaalisen ohjelmakoodin. Silloin ohjelmoijan työ muuttuu koodin kirjoittamisesta spesifikaation ristiriidattomuuden osoittamiseksi konstruktiivisesti, jotta todistuksesta voidaan tuottaa sitä vastaava ohjelmakoodi. Nämä välineet eivät vielä ole nousseet tutkimuslaboratorioista ohjelmistoteollisuuteen. Ehkä joskus tulevaisuudessa? Ehkä sellaisissa ohjelmointiongelmissa, joissa ohjelman oikeellisuus on kriittistä ja sen testaaminen vaikeaa? Jos näin käy, niin sen mukana funktionaalisen ohjelmoinnin rooli korostuu. 146