TIEA341 Funktio-ohjelmointi 1, kevät 2008 Luento 9 Kombinaattoreista Antti-Juhani Kaijanaho Jyväskylän yliopisto Tietotekniikan laitos 21. tammikuuta 2008
Currying Haskell-funktio ottaa aina vain yhden parametrin: f : : p a r a m e t r i t y y p p i > p a l u u t y y p p i f p a r a m e t r i = p a l u u a r v o... f a r g u m e n t t i... Mutta funktio voi palauttaa funktion: f : : p a r a m e t r i t y y p p i 1 > ( p a r a m e t r i t y y p p i 2 > p a l u u t y y p p i ) f p a r a m e t r i 1 = \ p a r a m e t r i 2 > p a l u u a r v o... ( f a r g u m e n t t i 1 ) a r g u m e n t t i 2
Julistetaan, että funktion soveltaminen assosioi vasemmalle ja funktiotyypin nuoli oikealle. Nyt tämä f : : p a r a m e t r i t y y p p i 1 > ( p a r a m e t r i t y y p p i 2 > p a l u u t y y p p i ) f p a r a m e t r i 1 = \ p a r a m e t r i 2 > p a l u u a r v o... ( f a r g u m e n t t i 1 ) a r g u m e n t t i 2 on sama kuin f : : p a r a m e t r i t y y p p i 1 > p a r a m e t r i t y y p p i 2 > p a l u u t y y p p i f p a r a m e t r i 1 = \ p a r a m e t r i 2 > p a l u u a r v o... f a r g u m e n t t i 1 a r g u m e n t t i 2
Julistetaan vielä, että f p a r a m e t r i 1 = \ p a r a m e t r i 2 > p a l u u a r v o on sama kuin f p a r a m e t r i 1 p a r a m e t r i 2 = p a l u u a r v o niin saadaan f : : p a r a m e t r i t y y p p i 1 > p a r a m e t r i t y y p p i 2 > p a l u u t y y p p i f p a r a m e t r i 1 p a r a m e t r i 2 = p a l u u a r v o... f a r g u m e n t t i 1 a r g u m e n t t i 2 Mutta funktiolla on edelleen vain yksi parametri (mutta se saattaa palauttaa funktion, joka syö myöhempiä parametreja).
idea on nimetty loogikko Haskell B. Curryn (1900 1982) mukaan idean keksivät kuitenkin aiemmin Gottlob Frege (1848 1925) ja Moses Schönfinkel (1889 1942) huomaa vakiokirjaston funktiot curry : : ( ( a, b ) > c ) > a > b > c curry f x y = f ( x, y ) uncurry : : ( a > b > c ) > ( ( a, b ) > c ) uncurry f p = f ( f s t p ) ( snd p )
η-muunnos 1 Seuraavat funktiot f ja g ovat sama funktio. f x = (... ) x g = (... ) Esimerkkejä vakiokirjastosta: sum, product : : (Num a ) => [ a ] > a sum = f o l d l (+) 0 product = f o l d l ( ) 1 1 η on kreikkalainen kirjain eta
Kombinaattori Määritelmä: Kombinaattori on funktio, jonka kaikki parametrit ovat (ensimmäisen tai korkeamman kertaluvun) funktioita Kombinaattori yleensä palauttaa funktion Haskellissa näin ollen se, onko joku funktio kombinaattori, on yleensä käyttötavasta, ei määritelmästä, riippuvaa.
Vakiokirjaston kombinaattori: (.) (. ) : : ( b > c ) > ( a > b ) > ( a > c ) f. g = \ x > f ( g x ) Kyseessä on matematiikan pallo-operaattori eli funktioiden kompositio. Käyttöesimerkkejä vakiokirjastosta: concatmap : : ( a > [ b ] ) > [ a ] > [ b ] concatmap f = concat. map f any, a l l : : ( a > Bool ) > [ a ] > Bool any p = or. map p a l l p = and. map p
Vakiokirjaston kombinaattori: flip f l i p : : ( a > b > c ) > ( b > a > c ) f l i p f = \x y > f y x flip siis vaihtaa parametrifunktionsa kahden ekan parametrin järjestystä. Käyttöesimerkkejä vakiokirjastosta: r e v e r s e : : [ a ] > [ a ] r e v e r s e = f o l d l ( f l i p ( : ) ) [ ] s u b t r a c t : : (Num a ) => a > a > a s u b t r a c t = f l i p ( ) Muita esimerkkejä: foreachm : : [ a ] > ( a > IO b ) > IO [ b ] foreachm = f l i p mapm foreachm_ : : [ a ] > ( a > IO b ) > IO ( ) foreachm_ = f l i p mapm_
Kombinaattorikirjastoista yksi funktio-ohjelmoinnin keskeisimmistä käsitteistä on kombinaattorikirjasto kirjasto koostuu muutamasta kombinaattorista, jotka pelaavat yhteen tuloksena syntyy ns. DSL ( domain-specific language )
ParComp on kombinaattorikirjasto data P a r s e r tok sem = P a r s e r ( [ tok ] > [ ( sem, [ tok ] ) ] ) (@ ) : : P a r s e r tok a > P a r s e r tok a > P a r s e r tok a (@@) : : P a r s e r tok a > ( a > b ) > P a r s e r tok b (@>) : : P a r s e r tok a > P a r s e r tok ( a > b ) > P a r s e r tok b Parser tok sem on funktiotyyppi!... tai siis isomorfinen funktiotyypin kanssa... konkreettisesti [tok] > [(sem, [tok])] mutta myös abstraktisti: funktio tok:sta sem:iin joten (@ ), (@@) ja (@>) ovat kombinaattoreita
ParComp on DSL domain (toimintapiiri, vaikutusalue) on kontekstittomat kieliopit tyyppiä Parser tok sem oleva arvo määrittelee kielen mitkä tok-jonot kuuluvat kyseiseen kieleen mikä tyyppiä sem oleva semanttinen arvo kuhunkin sallittuunjonoon liittyy
get :: Eq tok =>tok > Parser tok tok get hyväksyy kielen, johon kuuluu yksi merkkijono sen pituus on yksi ja se koostuu annetusta päätesymbolista; ko. merkkijonon semanttinen arvo on ko. päätesymboli
getid :: Parser Token String getid hyväksyy kielen, johon kuuluu yksi merkkijono sen pituus on yksi ja se koostuu nimestä (identifier) ko. merkkijonon semanttinen arvo on ko. nimi merkkijonona
(@>) :: Parser tok a > Parser tok (a > b) > Parser tok b (@>) yhdistää kaksi jäsennintä kompositiona p @> q hyväksyy kielen, jonka kukin merkkijono alkaa jollakin p:n merkkijonolla ja jatkuu (ja päättyy) jollakin q:n merkkijonolla q:n semanttisen arvon tulee olla funktio, joka ottaa p:n semanttisen arvon parametrinaan ja palauttaa uuden semanttisen arvon p @> q:n semanttinen arvo saadaan antamalla p:n semanttinen arvo q:n semanttiselle arvolle argumenttina
(@@) :: Parser tok a > (a > b) > Parser tok b (@@) muokkaa jäsentimen semanttista arvoa iso jekku tulee siitä, että (@@):llä on korkeampi presedenssi kuin (@>):llä, joka assosioi oikealle jos f :: t1 > t2 > t3, niin b @@ f :: t2 > t3 ja b :: t1, ja a @> b @@ f :: t3 ja a :: t2 siten lausekkeessa p1 @>... @> pn @@ f funktio f saa parametrinaan jäsentimien p1,..., pn semanttiset arvot käänteisessä järjestyksessä joten f toimii (S-)attribuuttikieliopin attribuuttilausekkeena!
(@ ) :: Parser tok a > Parser tok a > Parser tok a (@ ) ottaa kahden kielen yhdisteen jokainen tok-jono, joka kuuluu p:n tai q:n määrittelemään kieleen, kuuluu myös p @ q:n määrittelemään kieleen merkkijonon semanttinen arvo pysyy samana entä jos sama merkkijono kuuluu molempiin kieliin? silloin yhdistekielessä ko. merkkijonolla on useita (mahdollisesti) eri semanttista arvoa kumpi palautetaan määräytyy hyväntahtois-epädeterministisesti (engl. angelic nondeterminism) eli ei palauteta sitä, joka aiheuttaa jäsennyksen epäonnistumisen myöhemmin, mutta palautetaan jokin muista
expr -> cmp $1 expr -> if expr then expr else expr If $2 $4 $6 expr -> let Ident = expr in expr Let $2 $4 $6 e x p r : : P a r s e r Token Expr e x p r = cmp @@ (\ s1 > s1 ) @ g e t KW_If @> e x p r @> g e t KW_Then @> e x p r @> g e t KW_Else @> e x p r @@ (\ s6 _ s4 _ s2 _ > I f s2 s4 s6 ) @ get KW_Let @> g e t I d @> get Eq @> expr @> get KW_In @> expr @@ (\ s6 _ s4 _ s2 _ > L e t s2 s4 s6 )
ParComp:n toteutus data P a r s e r tok sem = P a r s e r ( [ tok ] > [ ( sem, [ tok ] ) ] ) Parser tok sem on funktio...... joka ottaa tok-jonon...... ja palauttaa listan kaikista mahdollisista listan alkuosan jäsennyksistä: kunkin jäsennyksen semanttisen arvon ja mitä tok-jonosta jäi jäsentämättä
get n ja getid n toteutus g e t I f : : ( t o k > Maybe a ) > P a r s e r t o k a g e t I f p = P a r s e r $ \ t o k s > c a s e t o k s o f [ ] > [ ] t : t s > c a s e p t o f J u s t sem > [ ( sem, t s ) ] N o t h i n g > [ ] g e t : : Eq t o k => t o k > P a r s e r t o k t o k g e t t o k = g e t I f $ \ x > i f x==t o k t h e n J u s t x e l s e N o t h i n g g e t I d : : P a r s e r Token S t r i n g g e t I d = g e t I f t e s t I d where t e s t I d ( I d e n t x ) = J u s t x t e s t I d _ = N o t h i n g yleinen funktio getif sen parametri palauttaa Just sem, jos kohdalle osuu oikea päätesymboli (sem on palautettava semanttinen arvo), muuten Nothing
(@ ) n toteutus (@ ) : : P a r s e r tok a > P a r s e r tok a > P a r s e r tok a P a r s e r p @ P a r s e r q = P a r s e r (\ t o k s > p t o k s ++ q t o k s ) tok-listan (toks) alkuosan kaikki mahdolliset jäsennykset saadaan ottamalla yhdiste (++)
(@@) n toteutus (@@) : : P a r s e r tok a > ( a > b ) > P a r s e r tok b P a r s e r p @@ f = P a r s e r $ \ t o k s > f l i p map ( p t o k s ) $ \ ( psem, p r e s t ) > ( f psem, p r e s t ) jokaisen mahdollisen jäsennyksen semanttiseen arvoon sovelletaan annettua funktiota
(@>) n toteutus (@>) : : Parser tok a > Parser tok ( a > b ) > Parser tok b P a r s e r p @> ~( P a r s e r q ) = P a r s e r $ \ t o k s > c o n c a t $ f l i p map ( p t o k s ) $ \ ( psem, p r e s t ) > f l i p map ( q p r e s t ) $ \ ( qsem, q r e s t ) > ( qsem psem, q r e s t ) tok-jono (toks) annetaan p-jäsentimelle sen tuottamalle jokaiselle jäsennystulokselle (psem,prest): jäljellä oleva tok-jono ( prest) annetaan q-jäsentimelle se tuottaa listan jäsennystuloksia kunkin tuloksen semanttiselle arvolle (joka on funktio) sovelletaan p:n tuottamaa semanttista arvoa (psem) tästä tuloksena on lista listoja, jotka yhdistetään concat-funktioilla
~hahmo on ns. laiska hahmo eli viivästetty hahmo hahmonsovitusta ei yritetä heti, vaan sovituksen oletetaan onnistuvan hahmonsovitusta yritetään vasta, kun hahmossa määriteltyä muuttujaa ekan kerran käytetään sovituksen epäonnistuminen kaataa ohjelman joten älä käytä tällaista muuttujaa, ellet ole varma että sovitus onnistuu! (@>)-operaattorin toinen parametri on viivästetty hahmo, jotta (ei-vasen-)rekursio toimisi!
runparser r u n P a r s e r : : Show t o k => P a r s e r t o k sem > [ t o k ] > E i t h e r S t r i n g sem runparser ( Parser p ) toks = f ( undefined, toks, maxbound ) ( p toks ) where f ( r e s, _, 0) _ = R i g h t r e s f ( r e s, t s, _) [ ] = L e f t ( " p a r s e e r r o r n e a r " ++ show t s ) f ( r e s, t s, n ) ( ( sem, t s ) : r e s t ) n > m = f ( sem, t s, m) r e s t o t h e r w i s e = f ( r e s, t s, n ) r e s t where m = l e n g t h t s funktio, joka ottaa jäsentimen ja tok-jonon ja palauttaa joko virheilmoituksen tai onnistuneen jäsennyksen semanttisen arvon apufunktio f etsii sen jäsennystuloksen, jolla on lyhin häntä (jäsentämättä jäänyt loppumerkkijono) jos häntää ei ole, jäsennys onnistui jos onnistuneita jäsennyksiä oli useita, funktio valitsee yhden jos häntää oli, merkkijono ei kuulu kieleen ja virhe lienee lähellä sitä kohtaa, johon jäsennys päättyy (arvaus!)
ParComp: plussat ja miinukset + jäsennin on isomorfinen kieliopin kanssa + toimii, vaikka kielioppi olisikin moniselitteinen + ei rajoitusta lookaheadille naivi algoritmi, tehoton yksiselitteisillä kieliopeilla vaatii vasemman rekursion poiston
Kehitysideoita lasketaan joka jäsentimelle FIRST-joukot voidaan raakata pois heti ilmiselvästi toimimattomat vaihtoehdot määrittelemällä Parser-tyyppi ja kombinaattorit toisin voisi ParComp-jäsentimestä tuottaa ulos kielioppikaaviot (ns. raidekaaviot ) sopivasti määrittelemällä samalla kirjastolla voisi saada aikaan sekä jäsentimen että kaaviot
newtype käärötyypit kuten Parser käyttävät vain osaa datan ilmaisuvoimasta kääntäjän optimointeja auttaa, jos tällaiset määritellään newtype-avainsanalla: newtype P a r s e r t o k sem = P a r s e r ( [ t o k ] > [ ( sem, [ t o k ] ) ] ) newtype-tyypillä saa olla vain yksi koostin... ja ko. koostimella saa olla vain yksi argumentti pieni ero semantiikassa, johon palattaneen myöhemmin