5.5. JÄSENNINKOMBINAATTOREISTA 67 type Env α = FiniteMap String α data EnvT m α = MkE (Env Integer m (Env Integer, α)) instance Transformer EnvT where promote mp = MkE $ λenv mp λr return $(env, r) instance Monad m Monad (EnvT m) where return = promote return (MkE p) f = MkE $ λenv p env λ(env, pr) case f pr of (MkE q) q env fail = promote fail instance Monad m EnvMonad (EnvT m) where begin (MkE p) = MkE $ λenv p env λ(_, r) return (env, r) bind v n = MkE $ λenv return (addtofm env v n, ()) getvalue v (MkE f) = MkE $ λenv case lookupfm env v of Just n return (env, n) Nothing f env Kuva 5.2: Ympäristömonadimuunnin 5.5 Jäsenninkombinaattoreista Tarkastellaanpa 6 nyt ongelman toista puolta. Miten jäsentää merkkijono niin, että saamme ulos sitä vastaavan tyyppiä Expr olevan abstraktin syntaksipuun? Tutkitaanpa ensin yleistä jäsennysongelmaa: tunnistetaan merkkijono ja kootaan sen perusteella semanttinen arvo. Jäsennin on siis oikeastaan funktio String α. Toisaalta joillakin merkkijonoilla voi olla useita semanttisia arvoja (jos kielen syntaksi on moniselitteinen), joten voisi olla parempi käyttää funktiotyyppiä String [α]. Aiemmin luennoilla on puhuttu kombinatorisesta ohjelmoinnista, jossa ohjelmia koostetaan toisista ohjelmista. Tämä oli monadisen lähestymistavan ydin. Nokkelina saattaisimme päätyä ajatukseen, että ehkä jäsentimetkin kannattaisi rakentaa kombinatorisesti: ainakin säännöllisten kielten tapauksessa tämä olisi mahdollista. Säännölliset kielethän voidaan kuvata säännöllisillä lausekkeilla: Määritelmä 5.5.1 Säännölliset lausekkeet ovat seuraavanlaisia: 6. Kirjoittaja pahoittelee tämän osan karuutta. Ehkä ensi vuonna sitten :)
68 LUKU 5. MONADIT 1. Symboli ε muodostaa yksinään säännöllisen lausekkeen. 2. Mikä tahansa aakkoston Σ merkki on säännöllinen lauseke. 3. Jos r on säännöllinen lauseke, niin r on säännöllinen lauseke (Kleenen tähti). 4. Jos r on säännöllinen lauseke, niin r + on säännöllinen lauseke. 5. Jos r ja s ovat säännöllisiä lausekkeita, niin rs on säännöllinen lauseke (r:n ja s:n katenointi). 6. Jos r ja s ovat säännöllisiä lausekkeita, niin r s on säännöllinen lauseke. Aakkoston Σ säännöllisten lausekkeiden joukkoa merkitään RE(Σ). Säännöllisen lausekkeen merkitys määritellään funktion L : RE(Σ) P(Σ ) avulla 7 : L ε = ε L c = c L rs = { vw v L r w L s } L r s = L r L s L r = L ε rr L r + = L rr missä c Σ Huomaa, että Kleenen tähden merkitys määritellään rekursiivisesti. Aivan formaalisti tämän tekeminen vaatisi kiintopisteoperaattoria, mikä ei valitettavasti kuulu tämän kurssin esitietoihin. Voisimme asettaa seuraavanlaiset Haskell-määritelmät: type Parser α = String [α] eps :: α Parser α eps a s = [a] char :: Char Parser Char char c [] = [] char c (x : _) c == x = [c] otherwise = [] 7. Sulut on tavanomainen tapa erottaa tarkasteltava kieli (tässä säännölliset lausekkeet) metakielestä (tässä merkkijonoteoria). Sulkujen sisällä oleva tavara on tarkasteltavaa kieltä, sulkujen ulkopuolella on metakieltä.
5.5. JÄSENNINKOMBINAATTOREISTA 69 ja niin edelleen. Ongelmaksi muodostuu katenoinnin määritteleminen. Tähän mennessä käyttämämme jäsentimen tyyppi ei mitenkään paljasta, mitä jäsennin on syötteestä syönyt. Siksi lienee parasta määritellä vielä kerran uudelleen: type Parser α = String [(α, String)] eps :: α Parser α eps a s = [(a, s)] char :: Char Parser Char char c [] = [] char c (x : xs) c == x = [(c, xs)] otherwise = [] Mutta lienee fiksumpaa tehdä jäsentimestä abstrakti tyyppi: newtype Parser α = MkP(String [(α, String)]) Ei ehkä niin yllättävästi näin määritelty jäsennintyyppi on monadi: instance Monad Parser where return x = MkP $ λs [(x, s)] (MkP p) f = MkP $ λs concatmap g $ p s where g (rv, rest) = case f rv of MkP p p rest fail _ = mzero Se kuuluu myös tyyppiluokkaan MonadPlus, joka on monadi, jolla on lisäksi yhteenlaskuoperaattori ja nolla : instance MonadPlus Parser where mzero = MkP $ λ_ [] (MkP p) mplus (MkP q) = MkP $ λs p s ++ q s Ajatus on se, että return x vastaa säännöllistä lauseketta ε, jonka semanttinen arvo on x ja p λx q on p:n ja q:n katenointi niin, että x:ään tulee p:n semanttinen arvo ja q tuottaa omansa lisäksi koko lausekkeen yhteisen semanttisen arvon. Kuvissa 5.3 ja 5.4 on tämän ajatuksen pohjalta määritelty jäsenninkombinaattorimoduli. Tämä moduli riittääkin edellä esitetyn laskimen jäsentimen rakentamiseen (ks. kuva 5.5). Kannattaa huomata, että vaikka edellä keskityttiinkin vain säännöllisten lausekkeiden kuvaamiseen, Haskellin rekursiiviset määritelmät antavat
70 LUKU 5. MONADIT module ParComb where import Char (isspace, isalpha, isdigit) import Monad (MonadPlus, mzero, mplus) newtype Parser a = MkP (String -> [(a, String)]) runparser :: Parser a -> String -> [(a, String)] runparser (MkP f) = f instance Monad Parser where return x = MkP $ \ s -> [(x, s)] (MkP p) >>= f = MkP $ \ s -> concatmap g $ p s where g (rv, rest) = case f rv of MkP p -> p rest fail _ = mzero instance MonadPlus Parser where mzero = MkP $ \ _ -> [] (MkP p) mplus (MkP q) = MkP $ \ s -> p s ++ q s eof :: a -> Parser a eof rv = MkP f where f [] = [(rv, [])] f (_:_) = [] eps :: a -> Parser a eps a = MkP $ \ s -> [(a, s)] maximal :: Parser [a] -> Parser [a] maximal (MkP p) = MkP $ \ s -> findmax (\ (a,s) -> length a) $ p s where findmax :: (a -> Int) -> [a] -> [a] findmax m l = let f (_, r) [] = r f p@(lr, r) (x:xs) lx > lr = f (lx, [x]) xs otherwise = f p xs where lx = m x in f (-1, []) l Kuva 5.3: Moduli ParComb alkaa
5.5. JÄSENNINKOMBINAATTOREISTA 71 item :: Parser Char item = MkP f where f [] = [] f (x:xs) = [(x, xs)] sat :: (Char -> Bool) -> Parser Char sat p = item >>= filt where filt c p c = return c otherwise = mzero char :: Char -> Parser Char char c = sat (==c) wchar :: Char -> Parser () wchar c = wsp >> char c >> return () string :: String -> Parser String string rv@([]) = return rv string rv@(x:xs) = char x >> string xs >> return rv wsp :: Parser () wsp = kstar (sat isspace) >> return () wstring :: String -> Parser String wstring s = wsp >> string s -- Kleene star kstar :: Parser a -> Parser [a] kstar p = eps [] mplus (p >>= \ x -> kstar p >>= \ xs -> return $ x:xs) -- Kleene plus kplus :: Parser a -> Parser [a] kplus p = p >>= \x -> kstar p >>= \xs -> return $ x:xs ident :: Parser String ident = maximal $ kplus (sat isalpha) wident :: Parser String wident = wsp >> ident int :: Parser Integer int = (maximal $ kplus (sat isdigit)) >>= return. (read :: String -> Integer) wint :: Parser Integer wint = wsp >> int Kuva 5.4: Moduli ParComb päättyy
72 LUKU 5. MONADIT meille ilmaiseksi kyvyn kuvata kontekstittomia kielioppeja. Ainoa rajoite on se, että tämä tekniikka ei kestä vasenrekursiivisia kielioppeja, joten vasen rekursio on ensin kieliopista poistettava. Kuvaa 5.5 vastaava konkreetti kielioppi on seuraavanlainen: Let-expression = "let", Identifier, "=", Expression, "in", Expression Expression Expression = Term, Rest of expression Rest of expression = (* nothing *) "+", Term, Rest of expression -", Term, Rest of expression Term = Factor, Rest of term Rest of term = (* nothing *) "*", Factor, Rest of term "/", Factor, Rest of term Factor =Identifier Integer literal "(", Let expression, ")" Kannattaa huomata, että tässä esitetty toteutus jäsenninkombinaattoreille on hyvin voimallinen mutta tehoton. Ks. esimerkiksi sivulle http://www.cs.uu.nl/ daan/parsec.html, jossa esitellään tehokas versio samasta ideasta.
5.5. JÄSENNINKOMBINAATTOREISTA 73 module Parser (parse) where import Expr import List (nub) import ParComb import Monad (mplus) parse :: String -> [Expr] parse s = nub $ map fst $ runparser (lexpr >>= eof) s lexpr :: Parser Expr lexpr = letex mplus expr letex :: Parser Expr letex = do wstring "let" x <- wident wchar = e <- expr wstring "in" e <- lexpr return $ Let x e e expr :: Parser Expr expr = term >>= expr expr :: Expr -> Parser Expr expr t = mplus (eps t) $ do op <- (wchar + >> return (:+)) mplus (wchar - >> return (:-)) t <- term expr (op t t ) term :: Parser Expr term = factor >>= term term :: Expr -> Parser Expr term f = mplus (eps f) $ do op <- (wchar * >> return (:*)) mplus (wchar / >> return (:/)) f <- factor term (op f f ) factor :: Parser Expr factor = (wident >>= return. Var) mplus (wint >>= return. Const) mplus (wchar ( >> lexpr >>= \r -> wchar ) >> return r) Kuva 5.5: Moduli Parser