5.3. LASKIMEN MUUNNELMIA 57 Samaan sarjaan kuuluu seuraavakin funktio, jonka määritelmä esittelee muutenkin hyödyllisen tavan kirjoittaa ohjelmia: getline :: IO String getline = getchar λc case c of \n return "\n" _ getline λl return (c : l) Kun rivinä on foo λx, sitä voidaan ajatella erikoisena tapana kirjoittaa imperatiivisista kielistä tuttu rivi x := foo;. Kombinaattori yhdistettynä λ- lausekkeeseen kun on tapa ilmaista jokin suoritettava ohjelma(lause), jonka tulos sijoitetaan muuttujaan x. Tämän vuoksi Haskellissa on kielioppimakeinen tällaisten kirjoittamiseen: muotoa do { P; Q } oleva lauseke tarkoittaa lauseketta P (do { Q }) ja muotoa do { x P; Q } oleva lauseke tarkoittaa (suunnilleen) lauseketta P λx (do { Q }). Kuten tavallista, myös tässä voidaan käyttää sisennystekniikkaa sulkujen ja puolipisteiden kera. Tällä tavalla ilmaistuna edellinen ohjelma olisi muotoa getline :: IO String getline = do c getchar case c of \n return "\n" _ do l getline return (c : l) Näyttää aika imperatiiviselta, vai mitä? 5.3 Laskimen muunnelmia Olkoon meillä tehtävänä toteuttaa yksinkertaisen laskimen laskentaosa. Joku toinen toteuttaa meille jäsentimen, joka tekee lausekkeista abstrakteja syntaksipuita. Voisimme vastata tehtävään tällaisella modulilla: module Eval where data Expr = Const Integer Expr :+ Expr Expr :- Expr Expr :* Expr Expr :/ Expr deriving Show eval :: Expr -> Integer
58 LUKU 5. MONADIT eval (Const n) = n eval (n :+ m) = eval n + eval m eval (n :- m) = eval n - eval m eval (n :* m) = eval n * eval m eval (n :/ m) = eval n div eval m Kaikki on helppoa, eikö? Ja luulisi tuon toimivankin. Vaan ei toimi: _ / _ \ /\ /\/ (_) / /_\// /_/ / / GHC Interactive, version 6.0.1, for Haskell 98. / /_\\/ / / http://www.haskell.org/ghc/ \ /\/ /_/\ / _ Type :? for help. Loading package base... linking... done. Compiling Eval ( Eval.hs, interpreted ) Ok, modules loaded: Eval. *Eval> eval (Const 4 :/ Const 0) *** Exception: divide by zero *Eval> Emme olleet ottaneet huomioon nollalla jaon mahdollisuutta! Vaikka GHCi sen ystävällisesti meidän puolestamme huomasikin, ei sen varaan voi heittäytyä: jos tämä moduli myöhemmin liitetään osaksi hienoa ACME Handy- Dandy Calculator 2004 -ohjelmistoa, ei tämä moduli, kuten ei mikään muukaan sen moduli, saa hajota hallitsemattomasti. Sen tulee välittää virhe kutsujalleen hoideltavaksi. Niinpä kirjoitamme siitä seuraavan version: module EvalV where data Expr = Const Integer Expr :+ Expr Expr :- Expr Expr :* Expr Expr :/ Expr deriving Show data Throws res exc = Return res Throw exc deriving Show eval :: Expr -> Throws Integer String eval (Const n) = Return n eval (a :+ b) = case (eval a, eval b) of (Return n, Return m) -> Return (n + m)
5.3. LASKIMEN MUUNNELMIA 59 eval (a :- b) = case (eval a, eval b) of (Return n, Return m) -> Return (n - m) eval (a :* b) = case (eval a, eval b) of (Return n, Return m) -> Return (n * m) eval (a :/ b) = case (eval a, eval b) of (Return n, Return 0) -> Throw "divide by zero" (Return n, Return m) -> Return (n div m) Nyt myös nollalla jako käyttäytyy hallitusti: _ / _ \ /\ /\/ (_) / /_\// /_/ / / GHC Interactive, version 6.0.1, for Haskell 98. / /_\\/ / / http://www.haskell.org/ghc/ \ /\/ /_/\ / _ Type :? for help. Loading package base... linking... done. Compiling EvalV ( EvalV.hs, interpreted ) Ok, modules loaded: EvalV. *EvalV> eval (Const 4 :/ Const 0) Throw "divide by zero" *EvalV> Kuitenkin eval-funktion koodi on muuttunut paljon rumemmaksi kuin mitä se oli ensimmäisessä esimerkissä. Vielä rumemmaksi se muuttuu, jos lisäämme tuen let-lausekkeille: 3 module EvalL where import FiniteMap data Expr = Const Integer Var String Expr :+ Expr Expr :- Expr Expr :* Expr Expr :/ Expr Let String Expr Expr deriving Show data Throws res exc = Return res Throw exc 3. FiniteMap on käsitelty tehtävässä 1.
60 LUKU 5. MONADIT deriving Show eval :: Expr -> Throws Integer String eval = let f :: FiniteMap String Integer -> Expr -> Throws Integer String f fm (Const n) = Return n f fm (Var s) = case lookupfm fm s of Just n -> Return n Nothing -> Throw $ "undefined variable " ++ s f fm (a :+ b) = case (eval a, eval b) of (Return n, Return m) -> Return (n + m) f fm (a :- b) = case (eval a, eval b) of (Return n, Return m) -> Return (n - m) f fm (a :* b) = case (eval a, eval b) of (Return n, Return m) -> Return (n * m) f fm (a :/ b) = case (eval a, eval b) of (Return _, Return 0) -> Throw "divide by zero" (Return n, Return m) -> Return (n div m) f fm (Let x e e ) = case eval e of t@(throw _) -> t Return n -> f (addtofm fm x n) e in f emptyfm Sotkuisuudesta huolimatta tuo oli suhteellisen helppo kirjoittaa matkien edellisen version ajattelua. Ja se näyttäisi toimivankin: _ / _ \ /\ /\/ (_) / /_\// /_/ / / GHC Interactive, version 6.0.1, for Haskell 98. / /_\\/ / / http://www.haskell.org/ghc/ \ /\/ /_/\ / _ Type :? for help. Loading package base... linking... done. Compiling RedBlackTree ( RedBlackTree.hs, interpreted ) Compiling FiniteMap ( FiniteMap.hs, interpreted ) Compiling EvalL ( EvalL.hs, interpreted ) Ok, modules loaded: EvalL, FiniteMap, RedBlackTree. *EvalL> eval (Const 4 :/ Const 0) Throw "divide by zero" *EvalL> eval (Let "x" (Const 4 :/ Const 0) (Var "x")) Throw "divide by zero" *EvalL> eval (Let "x" (Const 4 :/ Const 2) (Var "x")) Return 2
5.3. LASKIMEN MUUNNELMIA 61 Mutta ei kuitenkaan toimi: *EvalL> eval (Let "x" (Const 4 :/ Const 2) (Var "x" :+ Const 1)) Throw "undefined variable x" *EvalL> Mikä meni pieleen? Ikään kuin tieto määritellyistä muuttujista ei välittyisi :+operaattorin läpi sen alilausekkeille. Ovatko rekursiiviset kutsut kunnossa? Katsotaanpa muistin virkistykseksi yhtä tapausta: f fm (a :+ b) = case (eval a, eval b) of (Return n, Return m) -> Return (n + m) Kyllä siinä näyttäisi kutsuttavan eval-funktiota niin kuin pitääkin... hei hetkinen! Eihän eval ota lainkaan FiniteMapia parametrikseen, eikä sille sitä anneta. Muuttujamäärittelyt todella heitetään pois rekursiivisissa kutsuissa! Mutta miksi sitten ensimmäinen let-esimerkki toimi? Noh, näyttäisi siltä f fm (Let x e e ) = case eval e of t@(throw _) -> t Return n -> f (addtofm fm x n) e että let-tapauksessa FiniteMap välitetään korrektisti seuraavalle rekursiotasolle (mutta vain e :n tapauksessa!). Nyt täytyykin vain muuttaa kaikki muutkin rekursiokutsut vastaavasti: f fm (a :+ b) = case (f fm a, f fm b) of (Return n, Return m) -> Return (n + m) Nyt se toimii. Edellisen ohjelmointivirheen ja yleisen estetiikan tajun vuoksi herää kysymys siitä, olisiko alkuperäisen ohjelman rakentaa sellaiseksi, että sen laajentaminen näin olisi suhteellisen helppoa ja ei niin virhealtista ja että laajennetut versiot ovat suunnilleen yhtä kivoja lukea kuin alkuperäinen? Kuten Philip Wadler kirjoittaa 4 : The essence of an algorithm can become buried under the plumbing required to carry data from its point of creation to its point of use. Kuinka voisimme haudata putkistot maan alle algoritmin idean asemesta? 4. Philip Wadler: Monads for functional programming. In M. Broy, editor, Marktoberdorf Summer School on Program Design Calculi, Springer Verlag, NATO ASI Series F: Computer and systems sciences, Volume 118, August 1992. Also in J. Jeuring and E. Meijer, editors, Advanced Functional Programming, Springer Verlag, LNCS 925, 1995. Some errata fixed August 2001, see http://www.research.avayalabs.com/user/wadler/topics/monads.html.
62 LUKU 5. MONADIT 5.4 Monadinen laskin Hieman yllättäen vastaus edelliseen kysymykseen on sukua aiemmin esitetylle siirrännän ongelman ratkaisulle: rakennetaan ohjelma primitiivisistä ohjelmista ja yhdistellään niitä peräkkäistyskombinaattorilla. Emme kuitenkaan käytä IO α -tyyppiä sillä ei ole tarvittavia primitiivejä vaan rakennamme omamme. Kaikkeja tyyppikoostimia, joiden arvot ovat ohjelmia, joita voidaan suorittaa peräkkäin niin, että jälkimmäinen saa edellisen lopetusarvon parametrikseen, sanotaan monadeiksi 5. Kaikki monadit kuuluvat Haskellissa Monad-tyyppiluokkaan, joka määritellään seuraavasti: class Monad m where ( ) :: m α (α m β) m β ( ) :: m α m β m β return :: α m α fail :: String m α m k = m λ _ k fail s = error s Kuten huomataan, operaattorille ( ) ja funktiolle fail on annettu oletustoteutukset, joten niitä ei välttämättä tarvitse määritellä itse. Monadeilta vaaditaan seuraavat ominaisuudet: return a k = k a m return = m m (λx k x h) = (m k) h Peruslaskimelle (jossa on nollallajakoon varauduttu) käy mikä tahansa monadi: eval :: Monad m => Expr -> m Integer eval (Const n) = return n eval (a :+ b) = do n <- eval a return $ n + m eval (a :- b) = do n <- eval a 5. Sana monadi on tässä merkityksessään peräisin tietojenkäsittelyteorian ja abstraktin matematiikan alalta nimeltä kategoriateoria.
5.4. MONADINEN LASKIN 63 eval (a :* b) eval (a :/ b) return $ n - m = do n <- eval a return $ n * m = do n <- eval a when (m == 0) $ fail "divide by zero" return $ n div m Huomautus 13 Funktio when :: Monad m Bool m () m () when p s = case p of { True s; False return () } kuuluu varuskirjaston moduliin Monad, joka pitää erikseen importata. Periaatteessa tuo toimisi IO α -monadissakin: _ / _ \ /\ /\/ (_) / /_\// /_/ / / GHC Interactive, version 6.0.1, for Haskell 98. / /_\\/ / / http://www.haskell.org/ghc/ \ /\/ /_/\ / _ Type :? for help. Loading package base... linking... done. Compiling EvalMV ( EvalMV.hs, interpreted ) Ok, modules loaded: EvalMV. *EvalMV> eval (Const 1) >>= print Loading package haskell98... linking... done. 1 *EvalMV> eval (Const 4 :/ Const 1) >>= print 4 *EvalMV> eval (Const 4 :/ Const 0) >>= print *** Exception: user error Reason: divide by zero *EvalMV> mutta tähän ei IO α -monadia oikeastaan tarvita. Muitakin varuskirjastoon kuuluvia monadeita voidaan harkita: Maybe α *EvalMV> eval (Const 4 :/ Const 2) :: Maybe Integer Just 2 *EvalMV> eval (Const 4 :/ Const 0) :: Maybe Integer Nothing [α]
64 LUKU 5. MONADIT *EvalMV> eval (Const 4 :/ Const 2) :: [Integer] [2] *EvalMV> eval (Const 4 :/ Const 0) :: [Integer] [] *EvalMV> [0,0,0,0] >> eval (Const 4 :/ Const 2) [2,2,2,2] *EvalMV> [0,0,0,0] >> eval (Const 4 :/ Const 0) [] Listamonadissa kukin ohjelma suoritetaan rinnakkain kullekin listan alkiolle erikseen. Yksinkertaisin (käytännössä) mahdollinen monadi on seuraavanlainen: newtype Id α = MkId α deriving Show instance Monad Id where return x = MkId x (MkId x) f = f x Tehtävä 2 Osoita, että Id α käy monadiksi. Tämä monadi ei kuitenkaan välitä tietoa virheestä. Tässä mielessä parempi olisi seuraava: data Throws α = Return α Throw String deriving Show instance Monad Throws where return x = Return x (Return x) f = f x (Throw s) _ = Throw s fail s = Throw s Toisaalta tämän monadin tarjoama poikkeuspalvelu olisi hyödyllinen lisä mihin tahansa monadiin, joka sitä ei itse tarjoa. Sen takia onkin kiva määritellä se monadimuuntimena kuvan 5.1 mukaisesti. Let-kykyinen laskin tarvitsee jytymmät työkalut. Se tarvitsee monadin, joka kykenee pitämään kirjaa määritellyistä muuttujista (eli ympäristöstä) sekä niiden määritelmien vaikutusalueista (scope):
5.4. MONADINEN LASKIN 65 class Transformer t where promote :: Monad m m α t m α data Throws α = Return α Throw String deriving Show newtype ThrowsT m α = MkT (m (Throws α)) runthrowst :: Monad m ThrowsT m α m (Throws α) runthrowst (MkT m) = m instance Transformer ThrowsT where promote mp = MkT $ do p mp return $ Return p instance Monad m Monad (ThrowsT m) where return = promote return fail s = MkT $ return $ Throw s (MkT mp) f = MkT $ do p mp case p of Return n case f n of MkT q q Throw s return $ Throw s Kuva 5.1: Poikkeusmonadimuunnin
66 LUKU 5. MONADIT class Monad m EnvMonad mwhere begin :: m α m α bind :: String Integer m () getvalue :: String m Integer m Integer Metodi begin ajaa argumenttina saamansa ohjelman nykyisen ympäristön kopiossa. Kun ohjelma päättyy, sen kopio ympäristöstä heitetään pois. Tämä mahdollistaa leksikaaliset vaikutusalueet (lexical scoping). Metodikutsu bind v n lisää nykyiseen ympäristöön sidonnan s n. Metodikutsu getvalue v f hakee muuttujaan v nykyisessä ympäristössä sidotun arvon ja palauttaa sen; jos ko. muuttujaa ei ole nykyisessä ympäiristössä sidottu mihinkään, se suorittaa ohjelman f. Näillä keinoilla saadaan aikaiseksi seuraavanlainen let-kykyinen laskin: eval :: EnvMonad m => Expr -> m Integer eval (Const n) = return n eval (Var s) = getvalue s $ fail "undefined variable" eval (a :+ b) = do n <- eval a return $ n + m eval (a :- b) = do n <- eval a return $ n - m eval (a :* b) = do n <- eval a return $ n * m eval (a :/ b) = do n <- eval a when (m == 0) $ fail "divide by zero" return $ n div m eval (Let x e e ) = do er <- eval e begin $ do bind x er eval e Kuten huomataan, laskimen koodi on lähes samanlainen kuin ensimmäisessä monadisessa laskimessa. Eräs EnvMonad saadaan aikaan yhdistämällä kuvassa 5.2 esitetty monadimuunnin johonkin toiseen monadiin.
5.4. MONADINEN LASKIN 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