Jäsennys TIEA341 Funktio ohjelmointi 1 Syksy 2005
Muistutus: Laskutehtävä ja tulos data Laskutehtava = Luku Double Yhteen Laskutehtava Laskutehtava Vahennys Laskutehtava Laskutehtava Tulo Laskutehtava Laskutehtava Osamaara Laskutehtava Laskutehtava deriving (Show) data Tulos = LukuTulos Double VirheTulos String deriving (Show)
Jäsennys (1) Jäsennyksen tehtävänä on muuttaa merkkijono laskutehtävän rakennepuuksi: String > Laskutehtava virhetilanteiden vuoksi kuitenkin String > Either String Laskutehtava Jäsennys on erittäin paljon tutkittu ja hyvin ymmärretty ongelma
Jäsennys (2) Tavallisesti jäsennysongelma jaetaan kahtia: merkkijonon paloittelu sanasiksi vakiot, välimerkit ym. sanaslistan jäsentäminen Laskimen sanaset: data Sananen = Vakio Double SulkuAuki SulkuKiinni Plus Miinus Kertaa Jako OutoMerkki Char deriving (Show)
palastele :: String > [Sananen] palastele (' ':xs) = palastele xs palastele ('\t':xs) = palastele xs palastele ('\n':xs) = palastele xs palastele ('(':xs) = SulkuAuki : palastele xs palastele (')':xs) = SulkuKiinni : palastele xs palastele ('+':xs) = Plus : palastele xs palastele ('*':xs) = Kertaa : palastele xs palastele ('/':xs) = Jako : palastele xs palastele (' ':xs@(c:_)) '0' <= c && c <= '9' = let (d, r) = luku xs in Vakio ( d) : palastele r palastele (' ':xs) = Miinus : palastele xs palastele xs@(c:cs) '0' <= c && c <= '9' = let (d, r) = luku xs in Vakio d : palastele r otherwise = OutoMerkki c : palastele cs palastele [] = []
luku :: String -> (Double, String) luku s = case span isdigit s of (n, '.':r) -> case span isdigit r of (m, r') -> (read (n++. ++m), r') (n, r) -> (read n, r) where isdigit c = '0' <= c && c <= '9'
Jäsennyksen teoriaa Oikeastaan Automaatit ja kieliopit kurssin asiaa, mutta tehdään sitä nyt käytännöllisesti
Peruskäsitteet Aakkosto Kieli (Kontekstiton) kielioppi Jäsennin bottom up top down ennustava jäsennin
Aakkosto Formaalisti: epätyhjä äärellinen joukko Käytännössä usein merkistö aika usein myös: sanasten joukko Aakkoston alkioista tehty äärellinen jono on merkkijono
Kieli Formaalisti: kieli on jokin merkkijonojen joukko ei enempää, ei vähempää kieleen sinänsä ei liity mitään tulkintaa tai rakennetta Aakkoston {0, 1} kieliä: {000, 001, 010, 011, 100, 101, 110, 111} {1111111, 000000}
Kielioppi Kielioppi on jonkin yksittäisen kielen formaali määritelmä Yleensä käytännössä kiinnostaa vain kontekstiton kielioppi Kontekstiton kielioppi rakentuu hierarkisuuden ja rekursiivisuuden varaan
Kontekstittoman kieliopin rakenne Kontekstiton kielioppi on joukko produktioita Kukin produktio koostuu välikesymbolista, nuolesta (oikealle) sekä jonosta välike ja päätesymboleja Esimerkki: binääriluku > 0 binääriluku > 1 binääriluku > binääriluku 0 binääriluku > binääriluku 1 välikesymboli päätesymboleita
Kontekstittoman kieliopin tulkinta Välikesymboli on jonkin kielen nimi kirjoita paperille jokin välikesymboli etsi kieliopista produktio, jossa on jokin paperilla oleva välikesymboli nuolen vasemmalla puolella korvaa paperilla ko. välikesymbolin nimi ko. produktion oikealla puolella toista kahta edellistä, kunnes paperilla ei ole enää välikesymboleja
(1) binääriluku > 0 (2) binääriluku > 1 (3) binääriluku > binääriluku 0 (4) binääriluku > binääriluku 1 binääriluku
(1) binääriluku > 0 (2) binääriluku > 1 (3) binääriluku > binääriluku 0 (4) binääriluku > binääriluku 1 binääriluku ==> (3) binääriluku 0
(1) binääriluku > 0 (2) binääriluku > 1 (3) binääriluku > binääriluku 0 (4) binääriluku > binääriluku 1 binääriluku ==> (3) binääriluku 0 ==> (4) binääriluku 1 0
(1) binääriluku > 0 (2) binääriluku > 1 (3) binääriluku > binääriluku 0 (4) binääriluku > binääriluku 1 binääriluku ==> (3) binääriluku 0 ==> (4) binääriluku 1 0 ==> (4) binääriluku 1 1 0
(1) binääriluku > 0 (2) binääriluku > 1 (3) binääriluku > binääriluku 0 (4) binääriluku > binääriluku 1 binääriluku ==> (3) binääriluku 0 ==> (4) binääriluku 1 0 ==> (4) binääriluku 1 1 0 ==> (1) 0 1 1 0 (merkkijonon 0110 yo. kieliopin mukainen johto)
(1) laskutehtävä > lukuvakio (2) laskutehtävä > ( laskutehtävä ) (3) laskutehtävä > laskutehtävä + laskutehtävä (4) laskutehtävä > laskutehtävä laskutehtävä (5) laskutehtävä > laskutehtävä * laskutehtävä (6) laskutehtävä > laskutehtävä / laskutehtävä laskutehtävä
(1) laskutehtävä > lukuvakio (2) laskutehtävä > ( laskutehtävä ) (3) laskutehtävä > laskutehtävä + laskutehtävä (4) laskutehtävä > laskutehtävä laskutehtävä (5) laskutehtävä > laskutehtävä * laskutehtävä (6) laskutehtävä > laskutehtävä / laskutehtävä laskutehtävä ==>(5) laskutehtävä * laskutehtävä
(1) laskutehtävä > lukuvakio (2) laskutehtävä > ( laskutehtävä ) (3) laskutehtävä > laskutehtävä + laskutehtävä (4) laskutehtävä > laskutehtävä laskutehtävä (5) laskutehtävä > laskutehtävä * laskutehtävä (6) laskutehtävä > laskutehtävä / laskutehtävä laskutehtävä ==>(5) laskutehtävä * laskutehtävä ==>(2) laskutehtävä * ( laskutehtävä )
(1) laskutehtävä > lukuvakio (2) laskutehtävä > ( laskutehtävä ) (3) laskutehtävä > laskutehtävä + laskutehtävä (4) laskutehtävä > laskutehtävä laskutehtävä (5) laskutehtävä > laskutehtävä * laskutehtävä (6) laskutehtävä > laskutehtävä / laskutehtävä laskutehtävä ==>(5) laskutehtävä * laskutehtävä ==>(2) laskutehtävä * ( laskutehtävä ) ==>(3) laskutehtävä * ( laskutehtävä + laskutehtävä )
(1) laskutehtävä > lukuvakio (2) laskutehtävä > ( laskutehtävä ) (3) laskutehtävä > laskutehtävä + laskutehtävä (4) laskutehtävä > laskutehtävä laskutehtävä (5) laskutehtävä > laskutehtävä * laskutehtävä (6) laskutehtävä > laskutehtävä / laskutehtävä laskutehtävä ==>(5) laskutehtävä * laskutehtävä ==>(2) laskutehtävä * ( laskutehtävä ) ==>(3) laskutehtävä * ( laskutehtävä + laskutehtävä ) ==>(1) 35 * ( laskutehtävä + laskutehtävä )
(1) laskutehtävä > lukuvakio (2) laskutehtävä > ( laskutehtävä ) (3) laskutehtävä > laskutehtävä + laskutehtävä (4) laskutehtävä > laskutehtävä laskutehtävä (5) laskutehtävä > laskutehtävä * laskutehtävä (6) laskutehtävä > laskutehtävä / laskutehtävä laskutehtävä ==>(5) laskutehtävä * laskutehtävä ==>(2) laskutehtävä * ( laskutehtävä ) ==>(3) laskutehtävä * ( laskutehtävä + laskutehtävä ) ==>(1) 35 * ( laskutehtävä + laskutehtävä ) ==>(1) ja (1) 35 * ( 12 + 4 )
Kertausta kieliopeista Kielioppi siis määrittelee jonkin tietyn kielen Koostuu produktioista Produktiolla on vasen ja oikea puoli Vasemmalla puolella on välikesymboli Oikealla puolella on pääte ja välikesymboleita Välikesymboli voidaan kieliopin avulla avata joksikin sen kieleen kuuluvaksi merkkijonoksi (tämä prosessi on epädeterministinen)
Rakennepuu (1) laskutehtävä (1) laskutehtävä (5) laskutehtävä ==>(5) laskutehtävä * laskutehtävä ==>(2) laskutehtävä * ( laskutehtävä ) ==>(3) laskutehtävä * ( laskutehtävä + laskutehtävä ) ==>(1) 35 * ( laskutehtävä + laskutehtävä ) ==>(1) ja (1) 35 * ( 12 + 4 ) laskutehtävä (2) laskutehtävä (1) laskutehtävä (3) laskutehtävä (1) 35 * ( 12 + 4 ) (lausekkeen 35*(12+4) niin sanottu konkreetti rakennepuu)
Rakennepuu (2) (Konkreetti) rakennepuu on puu, jonka lehtisolmut ovat päätesymboleja jonka sisäsolmut ovat välikesymboleja jossa jokaiselle sisäsolmulle pätee seuraavaa: kun ko. solmu katsotaan produktion vasemmaksi puoleksi ja sen lasten katsotaan muodostavan produktion oikean puolen löytyy tuo produktio kieliopista Konkreetista rakennepuusta on mahdollista lukea merkkijonon johto... sekä päin vastoin
laskutehtävä laskutehtävä laskutehtävä laskutehtävä laskutehtävä 2 + ( 3 * 4 ) laskutehtävä laskutehtävä laskutehtävä laskutehtävä laskutehtävä ( 2 + 3 ) * 4 Kaksi saman merkkijonon rakennepuuta saman kieliopin mukaan (punaiset sulut osoittavat, mitä nämä eri rakennepuut oikein tarkoittavat)
Moniselitteisyys Kielioppi on moniselitteinen, jos samalla merkkijonolla on sen mukaan ainakin kaksi erilaista rakennepuuta (ja siis johtoa) Moniselitteisyys on yleensä mahdollista poistaa muokkaamalla kielioppia muokattu kielioppi tuottaa saman kielen eli samat merkkijonot muokattu kielioppi ei kuitenkaan ole moniselitteinen
Moniselitteinen kielioppi (1) laskutehtävä > lukuvakio (2) laskutehtävä > ( laskutehtävä ) (3) laskutehtävä > laskutehtävä + laskutehtävä (4) laskutehtävä > laskutehtävä laskutehtävä (5) laskutehtävä > laskutehtävä * laskutehtävä (6) laskutehtävä > laskutehtävä / laskutehtävä Yksiselitteinen kielioppi (1) laskutehtävä > termi (2) laskutehtävä > laskutehtävä + termi (3) laskutehtävä > laskutehtävä termi (4) termi > tekijä (5) termi > termi * tekijä (6) termi > termi / tekijä (7) tekijä > lukuvakio (8) tekijä > ( laskutehtävä )
(S )attribuuttikieliopit Jokaiseen symboliin liittyy semanttinen arvo esim. lukuvakio symbolin semanttinen arvo voi olla 42, 12, 999 jne. Jokaiseen produktioon lisätään synteettinen attribuutti sääntö, jolla produktion vasemmanpuoleisen välikesymbolin semanttinen arvo johdetaan oikealla puolella esiintyvien symboleiden semanttisista arvoista
(1) laskutehtävä > termi $1 (2) laskutehtävä > laskutehtävä + termi Yhteen $1 $3 (3) laskutehtävä > laskutehtävä termi Vahennys $1 $3 (4) termi > tekijä $1 (5) termi > termi * tekijä Tulo $1 $3 (6) termi > termi / tekijä Osamaara $1 $3 (7) tekijä > lukuvakio Luku $1 (8) tekijä > ( laskutehtävä ) $2 $i tarkoittaa oikean puolen i:nnen symbolin semanttista arvoa tässä semanttiset arvot ovat Haskell lausekkeita merkkijonoa johdettaessa tulkinta: muodostetaan merkkijono, joka vastaa ko. semanttista arvoa produktiota saa käyttää vain jos semanttinen arvo on sovitettavissa sen attribuuttiin 1 + 2 * 3 on joko Yhteen (Luku 1) (Tulo (Luku 2) (Luku 3)) tai Tulo (Yhteen (Luku 1) (Luku 2)) (Luku 3)
Jäsennysongelma Annettu: kielioppi (mahdollisesti attribuutteineen) merkkijono Tulos: merkkijonon johto (rakennepuu) tai se semanttinen arvo, josta merkkijono on johdettu tai tieto siitä, ettei merkkijono kuulu kieleen syntaksivirhe
Toiveita jäsennysalgoritmista Mikäli mahdollista, toimii lineaarisesti skannaamalla annettua merkkijonoa ei palata taaksepäin (no backtracking) mahdollisimman kevyt muutenkin Mahdollisimman hyvä virheen diagnosointi jos merkkijono ei kuulu kieleen, miksi ei? voiko merkkijonoa vähän muuttamalla tehdä siitä kieleen kuuluvan?
Tunnettuja jäsennystapoja Alhaalta ylöspäin rakennetaan rakennepuu lehdistä juureen päin LR(1), LARL,... työkaluja: yacc, bison, happy, SableCC Ylhäältä alaspäin LL(1), ei vasenrekursiivisuus rekursiivisesti etenevä (usein ennustava) jäsennys usein kirjoitetaan käsin työkaluja: JavaCC, lucky,...
Rekursiivisesti etenevä jäsennys (1) recursive descent parsing tavallisesti käsin kirjoitettuja jäsentimiä idea: kutakin välikesymbolia vastaa funktio, joka koettaa katsoa, kuuluuko merkkijono sen määrittelemään kieleen funktio koettaa sovittaa merkkijonoa välikesymbolin produktioihin funktio kutsuu toisia funktioita, kun pitää selvittää, sopiiko jokin osamerkkijono johonkin toiseen välikesymboliin
Rekursiivisesti etenevä jäsennys (2) Käytettävä kielioppi ei saa olla vasenrekursiivinen vasen rekursio: produktion oikean puolen ensimmäinen symboli voidaan laajentaa muotoon, jossa sen alussa on ko. produktion vasen puoli esim. termi > termi + tekijä muuten rekursiivisesti etenevä jäsennin jää päättymättömään rekursioon Vasen rekursio on mahdollista poistaa ns. vasemmalla tekijöinnillä tuloksena vaan sotkuisempi kielioppi
(1) laskutehtävä > termi $1 (2) laskutehtävä > laskutehtävä + termi Yhteen $1 $3 (3) laskutehtävä > laskutehtävä termi Vahennys $1 $3 Vasen tekijöinti (4) termi > tekijä $1 (5) termi > termi * tekijä Tulo $1 $3 (6) termi > termi / tekijä Osamaara $1 $3 laskutehtävä > termi laskutehtävä' $2 $1 (7) tekijä > lukuvakio Luku $1 (8) tekijä > ( laskutehtävä ) $2 laskutehtävä' > \ x > x laskutehtävä' > + laskutehtävä \ x > Yhteen x $2 laskutehtävä' > laskutehtävä \ x > Vahennys x $2 termi > tekijä termi' $2 $1 termi' > \ x > x termi' > * termi \ x > Tulo x $2 termi' > / termi \ x > Osamaara x $2 tekijä > lukuvakio Luku $1 tekijä > ( laskutehtävä ) $2
Jäsentäminen Haskellissa Oikeaan työhön suosittelen oikeita työkaluja: Happy, http://www.haskell.org/happy/ Lucky, http://www.ki.informatik.uni frankfurt.de/~klose/lucky.html Parsec, http://www.cs.uu.nl/~daan/parsec.html Bnfc, http://www.cs.chalmers.se/~markus/bnfc/ Seuraavassa tehoton mutta opettavainen itse alusta asti rakennettu laskutehtävän jäsennin
Jäsentimen tyyppi? Epäilemättä funktio Ottaa sanasjonon Palauttaa sitä vastaavan semanttisen arvon jos kielioppi on moniselitteinen, useita sellaisia Palauttaa myös jäljelle jääneen sanasjonon type Parser a = [Sananen] > [(a, [Sananen])] Avainsana type määrittelee tyyppialiaksen: vasen tyyppinimi tarkoittaa samaa kuin oikealla esiintyvä tyyppi. Parser a on näin lyhennysmerkintä tyypille [Sananen] > [(a, [Sananen])]
Onnistuisiko näin? laskutehtävä > termi laskutehtävä' $2 $1 laskutehtävä' > \ x > x laskutehtävä' > + laskutehtävä \ x > Yhteen x $2 laskutehtävä' > laskutehtävä \ x > Vahennys x $2 termi > tekijä termi' $2 $1 termi' > \ x > x termi' > * termi \ x > Tulo x $2 termi' > / termi \ x > Osamaara x $2 tekijä > lukuvakio Luku $1 tekijä > ( laskutehtävä ) $2 laskutehtava :: Parser Laskutehtava laskutehtava' :: Parser (Laskutehtava > Laskutehtava) termi :: Parser Laskutehtava termi' :: Parser (Laskutehtava > Laskutehtava) tekija :: Parser Laskutehtava lukuvakio :: (Double > a) > Parser a syo :: Sananen > Parser ()
laskutehtävä > termi laskutehtävä' $2 $1 laskutehtava :: Parser Laskutehtava laskutehtava = termi `followedby` laskutehtava' `sem` \(p1,p2) > p2 p1 followedby :: Parser a > Parser b > Parser (a, b) followedby p1 p2 toks = concat (map (\(a,s) > map (\(b,s') > ((a,b),s')) (p2 s)) (p1 toks)) sem :: Parser a > (a > b) > Parser b sem p f toks = map (\(a,s) > (f a, s)) (p toks)
laskutehtävä' > \ x > x laskutehtävä ' > + laskutehtävä \ x > Yhteen x $2 laskutehtävä' > laskutehtävä \ x > Vahennys x $2 laskutehtava' :: Parser (Laskutehtava > Laskutehtava) laskutehtava' = (ota Plus `followedby` laskutehtava `sem` \(_,p) x > Yhteen x p) `orelse` (ota Miinus `followedby` laskutehtava `sem` \(_,p) x > Vahennys x p) `orelse` otatyhja (\x > x) orelse :: Parser a > Parser a > Parser a orelse p1 p2 toks = p1 toks ++ p2 toks ota :: Sananen > Parser () ota tok (tok':toks) tok == tok' = [((),toks)] ota = [] otatyhja :: a > Parser a otatyhja a toks = [(a, toks)] (termi, termi' samaan tapaan)
tekijä > lukuvakio Luku $1 tekijä > ( laskutehtävä ) $2 tekija :: Parser Laskutehtava tekija = lukuvakio Luku `orelse` (ota SulkuAuki `followedby` laskutehtava `followedby` ota SulkuKiinni `sem` \((_,l),_) > l) lukuvakio :: (Double > a) > Parser a lukuvakio f (Vakio d:toks) = [(f d,toks)] lukuvakio = []
Toimii, mutta... Tehoton: kokeilee kaikkia vaihtoehtoja rinnakkain fiksu, jos kielioppi on raskaasti moniselitteinen useimmiten tuhlaa resursseja (ei kylläkään peruuta!) Huono diagnostiikka: ainoat palautteet ovat moniselitteinen syöte ja syntaksivirhe
Parannusideoita Vältetään rinnakkaisuutta silloin, kun sen tiedetään olevan tarpeetonta FIRST ja FOLLOW joukot (ks. AuKi) Pidetään kirjaa virheistä esim. ota voisi hyvin kirjata odotin sanasta Foo, sain sanasen Bar ongelmana relevanttien virheiden seulonta turhista Ei kuulu enää tämän kurssin sisältöihin käyttäkää valmiita työkaluja ja kirjastoja : )
Yhteenveto: keskeiset käsitteet Kieli Kontekstiton kielioppi produktio välikesymboli päätesymboli Attribuutti, semanttinen arvo Rakennepuu Rekursiivisesti etenevä jäsennys