Estructurando código con Mónadas

Anuncio
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)
Descargar