Estructurando código con Mónadas Mauro Jaskelioff Jueves 20 de Octubre, 2015 Mónadas I Las mónadas proveen una forma de modelar entrada/salida en lenguajes puros (IO). I También vimos que proveen una generalización de substitución en mónadas de términos. I En esta clase veremos que son una forma efectiva y versátil de estructurar el código. En particular al estructurar el código con mónadas ganamos: I I I I Modularidad Reuso Claridad conceptual Un evaluador simple I El AST es: data Exp = Const Int | Plus Exp Exp | Div Exp Exp I El evaluador es: eval 1 :: Exp → Int eval 1 (Const n) = n eval 1 (Plus t u) = eval 1 t + eval 1 u eval 1 (Div t u) = eval 1 t ‘div ‘ eval 1 u I La división puede ocasionar un error en run-time que no manejamos. Un evaluador con manejo de error I Modificamos el evaluador para que maneje el error: eval 2 :: Exp → Maybe Int eval 2 (Const n) = Just n eval 2 (Plus t u) = case eval 2 t of Nothing → Nothing Just m → case eval 2 u of Nothing → Nothing Just n → Just (m + n) eval 2 (Div t u) = case eval 2 t of Nothing → Nothing Just m → case eval 2 u of Nothing → Nothing Just n → if n ≡ 0 then Nothing else Just (m ‘div ‘ n) I El evaluador ya no es tan simple. Contando operaciones I Modificamos el evaluador original para contar divisiones. eval 3 :: Exp → (Int, Int) eval 3 (Const n) = (n, 0) eval 3 (Plus t u) = let (m, cm) = eval 3 t (n, cn) = eval 3 u in (n + m, cm + cn) eval 3 (Div t u) = let (m, cm) = eval 3 t (n, cn) = eval 3 u in (n ‘div ‘ m, cm + cn + 1) Agregando variables I Modificamos el AST para poder tener variables. type Variable = String data Exp v = Const Int | Var Variable | Plus Exp v Exp v | Div Exp v Exp v I El evaluador recibe un entorno: type Env = Variable → Int eval 4 eval 4 eval 4 eval 4 eval 4 :: Exp v → Env → Int (Const n) ρ = n (Var v ) ρ = ρ v (Plus t u) ρ = eval 4 t ρ + eval 4 u ρ (Div t u) ρ = eval 4 t ρ ‘div ‘ eval 4 u ρ Combinando todo eval 5 :: Exp v → Env → Maybe (Int, Int) eval 5 (Const n) ρ = Just (n, 0) eval 5 (Var v ) ρ = Just (ρ v , 0) eval 5 (Plus t u) ρ = case eval 5 t ρ of Nothing → Nothing Just (m, cm) → case eval 5 u ρ of Nothing → Nothing Just (n, cn) → Just (m + n, cm + cn) eval 5 (Div t u) ρ = case eval 5 t ρ of Nothing → Nothing Just (m, cm) → case eval 5 u ρ of Nothing → Nothing Just (n, cn) → if n ≡ 0 then Nothing else Just (m ‘div ‘ n, cm + cn + 1) Evaluación de lo hecho I I Para cada modificación hubo que reescribir gran parte del código. Incluso cuando las modificaciones sólo afectaban a una pequeña parte: I I I Sólo Div puede ocasionar un error; Sólo Div agrega algo al contador; Sólo Var hace algo con el entorno. I Conclusión: esta forma de implementar es muy poco modular, dificulta el reuso, y se pierde la idea del programa en detalles. I Incluso en un lenguaje tan simple el evaluador combinado es muy complejo. ¡Mónadas al rescate! I Los diferentes evaluadores son esencialmente iguales: I I Realizan una computación que devuelve un entero. Esto lo podemos representar mediante un tipo m Int donde m es un constructor de tipos que modela la computación. I Necesitamos una forma de devolver el valor entero computado (return) I Necesitamos una forma de componer computaciones (>>=). I Como tipo de retorno usaremos una mónada m que modela el tipo de computación que queremos realizar. class Monad m where return :: a → m a (>>=) :: m a → (a → m b) → m b Mónada Identidad I Modela computaciones puras. newtype Id a = Id a runId :: Id a → a runId (Id x) = x instance Monad Id where return x = Id x Id x >>= f = f x I La mónada Identidad no agrega nada, pero funciona como tipo abstracto de datos, que expone sólo la interfaz de la mónada. Evaluador monádico I Reimplementamos eval 1 con la mónada identidad evalM 1 :: Exp → Id Int evalM 1 (Const n) = return n evalM 1 (Plus t u) = do m ← evalM 1 t n ← evalM 1 u return (m + n) evalM 1 (Div t u) = do m ← evalM 1 t n ← evalM 1 u return (m ‘div ‘ n) I I Notar que evalM 1 sólo usa las operaciones return y (>>=). Es más, el tipo más general del evaluador es: evalM 1 :: Monad m ⇒ Exp → m Int I Ejercicio: Probar que eval 1 = runId ◦ evalM 1 . Mónada Maybe I Modela computaciones que pueden fallar data Maybe a = Nothing | Just a instance Monad Maybe where return a = Just a Nothing >>= f = Nothing Just x >>= f = f x I Posee una operación con acceso a la representación interna que permite señalar errores. throw :: Maybe a throw = Nothing Evaluador Monádico con manejo de error I El evaluador monádico con manejo de errores es: evalM 2 :: Exp → Maybe Int evalM 2 (Const n) = return n evalM 2 (Plus t u) = do m ← evalM 2 t n ← evalM 2 u return (m + n) evalM 2 (Div t u) = do m ← evalM 2 t n ← evalM 2 u if n ≡ 0 then throw else return (m ‘div ‘ n) I El evaluador sólo utiliza las operaciones return, (>>=) y throw . I Con respecto a evalM 1 sólo modificamos el chequeo de división por 0. I Ejercicio: Probar que eval 2 = evalM 2 . Mónada de acumulación I Modela computaciones que llevan un acumulador newtype Acum a = Ac (a, Int) runAcum :: Acum a → (a, Int) runAcum (Ac p) = p instance Monad Acum where return x = Ac (x, 0) Ac (x, n) > >= f = let Ac (x 0 , n0 ) = f x in Ac (x 0 , n + n0 ) I Posee una operación para sumar 1 al acumulador. tick :: Acum () tick = Ac ((), 1) Evaluador monádico con contador I El evaluador monádico con contador es: evalM 3 :: Exp → Acum Int evalM 3 (Const n) = return n evalM 3 (Plus t u) = do m ← evalM 3 t n ← evalM 3 u return (m + n) evalM 3 (Div t u) = do m ← evalM 3 t n ← evalM 3 u tick return (m ‘div ‘ n) I El evaluador sólo utiliza las operaciones return, (>>=) y tick. I Con respecto a evalM 1 sólo agregamos un tick para contar la operación de división I Ejercicio: Probar que eval 3 = runAcum ◦ evalM 3 . Mónada de entorno I Modela computaciones que llevan un entorno newtype Reader a = Reader (Env → a) runReader :: Reader a → Env → a runReader (Reader h) = h instance Monad Reader where return x = Reader (λ → x) Reader h > >= f = Reader (λρ → runReader (f (h ρ)) ρ) I Posee una operación para obtener el entorno ask :: Reader Env ask = Reader (λρ → ρ) Evaluador monádico con variables I El evaluador monádico con variables es: evalM4 :: Exp v → Reader Int evalM4 (Const n) = return n evalM4 (Var v ) = do ρ ← ask return (ρ v ) evalM4 (Plus t u) = do m ← evalM4 t n ← evalM4 u return (m + n) evalM4 (Div t u) = do m ← evalM4 t n ← evalM4 u return (m ‘div ‘ n) I I I El evaluador sólo utiliza las operaciones return, (>>=) y ask. Con respecto a evalM 1 sólo agregamos el pattern Var . El resto de las lı́neas es exactamente igual. Ejercicio: Probar que para todo t, eval 4 t = runReader (evalM4 t). Evaluador monádico combinado I El evaluador combinado queda: evalM5 :: Exp v → M Int evalM5 (Const n) = return n evalM5 (Var v ) = do ρ ← ask return (ρ v ) evalM5 (Plus t u) = do m ← evalM5 t n ← evalM5 u return (m + n) evalM5 (Div t u) = do m ← evalM5 t n ← evalM5 u tick if n ≡ 0 then throw else return (m ‘div ‘ n) I Sólo hace falta una mónada M con throw , tick, y ask. Mónada para el evaluador combinado newtype M a = M (Env → Maybe (a, Int)) runM :: M a → Env → Maybe (a, Int) runM (M x) = x instance Monad M where return x = M (\ → Just (x, 0)) M h >>= f = M (λρ → case h ρ of Nothing → Nothing Just (a, m) → case runM (f a) ρ of Nothing → Nothing Just (b, n) → Just (b, m + n)) throw :: M a throw = M (\ → Nothing ) ask ask :: M Env = M (λρ → Just (ρ, 0)) tick :: M () tick = M (\ → Just ((), 1)) Observaciones I Las mónadas capturan una gran cantidad de efectos, hasta acá vimos, errores, entornos y acumulación. I I I I En la práctica veremos algunos más. Los evaluadores monádicos toman argumentos puros, pero devuelven un valor en una mónada. Las funciones que usan la mónada sólo usan la interfaz, es decir: return, (>>=) y operaciones propias de la mónada. Podemos hacer los evaluadores más generales definiendo clases de mónada que soportan determinadas operaciones. class Monad m ⇒ MonadThrow m where throw :: m a Luego el evaluador con manejo de errores queda definido para cualquier mónada que implemente throw : evalM 2 :: MonadThrow m ⇒ Exp → m Int Más Observaciones I I En versiones recientes de GHC, se requiere que toda instancia de mónada sea de la clase Applicative, y que todo Applicative sea Functor . Para dejar tranquilo al compilador podemos agregar a toda instancia de mónada M: instance Functor M where fmap = liftM instance Applicative M where pure = return (<∗>) = ap posiblemente con los siguiente imports import Control.Applicative (Applicative (. .)) import Control.Monad (liftM, ap) Resumen I Las mónadas son una abstracción muy útil para estructurar código. I Permiten la programación de código más modular y reusable. I Su uso no se limita a intérpretes. (aunque casi cualquier cosa puede ser vista como un intérprete) I Su uso no se limita a Haskell (aunque en general se usa en lenguajes con alto orden). I Las mónadas proveen un ; programable I No son las únicas estructuras que se utilizan (ver por ej. functores aplicativos y arrows) Referencias I Monads for Functional Programming. Philip Wadler (1995) I Introduction to Functional Programming. Richard Bird (1998) I The Craft of Functional Programming (2nd ed). Simon Thompson (1999)