Haskell Language
Transformadores de mónada
Buscar..
Un contador monádico
Un ejemplo sobre cómo componer el lector, el escritor y la mónada de estado utilizando transformadores de mónada. El código fuente se puede encontrar en este repositorio.
Queremos implementar un contador, que incremente su valor en una constante dada.
Comenzamos definiendo algunos tipos y funciones:
newtype Counter = MkCounter {cValue :: Int}
deriving (Show)
-- | 'inc c n' increments the counter by 'n' units.
inc :: Counter -> Int -> Counter
inc (MkCounter c) n = MkCounter (c + n)
Supongamos que queremos realizar el siguiente cálculo utilizando el contador:
- poner el contador a 0
- establece la constante de incremento a 3
- incrementar el contador 3 veces
- establece la constante de incremento a 5
- incrementar el contador 2 veces
La mónada estatal proporciona abstracciones para pasar el estado. Podemos hacer uso de la mónada de estado y definir nuestra función de incremento como un transformador de estado.
-- | CounterS is a monad.
type CounterS = State Counter
-- | Increment the counter by 'n' units.
incS :: Int-> CounterS ()
incS n = modify (\c -> inc c n)
Esto ya nos permite expresar un cálculo de una manera más clara y sucinta:
-- | The computation we want to run, with the state monad.
mComputationS :: CounterS ()
mComputationS = do
incS 3
incS 3
incS 3
incS 5
incS 5
Pero todavía tenemos que pasar la constante de incremento en cada invocación. Nos gustaría evitar esto.
Añadiendo un entorno
La mónada del lector proporciona una forma conveniente de pasar un entorno. Esta mónada se usa en la programación funcional para realizar lo que en el mundo OO se conoce como inyección de dependencia .
En su versión más simple, la mónada del lector requiere dos tipos:
el tipo de valor que se lee (es decir, nuestro entorno,
r
continuación),el valor devuelto por la mónada lector (
a
continuación).Lector ra
Sin embargo, también necesitamos hacer uso de la mónada estatal. Por lo tanto, necesitamos usar el transformador ReaderT
:
newtype ReaderT r m a :: * -> (* -> *) -> * -> *
Usando ReaderT
, podemos definir nuestro contador con el entorno y el estado de la siguiente manera:
type CounterRS = ReaderT Int CounterS
Definimos una función incR
que toma la constante de incremento del entorno (utilizando ask
), y para definir nuestra función de incremento en términos de nuestra mónada CounterS
utilizamos la función de lift
(que pertenece a la clase de transformador de mónada ).
-- | Increment the counter by the amount of units specified by the environment.
incR :: CounterRS ()
incR = ask >>= lift . incS
Usando la mónada del lector podemos definir nuestro cálculo de la siguiente manera:
-- | The computation we want to run, using reader and state monads.
mComputationRS :: CounterRS ()
mComputationRS = do
local (const 3) $ do
incR
incR
incR
local (const 5) $ do
incR
incR
Los requisitos cambiaron: necesitamos registro!
Ahora suponga que queremos agregar el registro a nuestro cálculo, de modo que podamos ver la evolución de nuestro contador a tiempo.
También tenemos una mónada para realizar esta tarea, la mónada escritora . Al igual que con la mónada del lector, ya que los estamos componiendo, necesitamos hacer uso del transformador de mónada del lector:
newtype WriterT w m a :: * -> (* -> *) -> * -> *
Aquí w
representa el tipo de salida a acumular (que debe ser un monoide, que nos permite acumular este valor), m
es la mónada interna y a
tipo de cálculo.
Luego podemos definir nuestro contador con registro, entorno y estado de la siguiente manera:
type CounterWRS = WriterT [Int] CounterRS
Y haciendo uso de lift
podemos definir la versión de la función de incremento que registra el valor del contador después de cada incremento:
incW :: CounterWRS ()
incW = lift incR >> get >>= tell . (:[]) . cValue
Ahora el cálculo que contiene el registro se puede escribir de la siguiente manera:
mComputationWRS :: CounterWRS ()
mComputationWRS = do
local (const 3) $ do
incW
incW
incW
local (const 5) $ do
incW
incW
Haciendo todo de una vez
Este ejemplo pretende mostrar los transformadores de mónada en funcionamiento. Sin embargo, podemos lograr el mismo efecto al componer todos los aspectos (entorno, estado y registro) en una sola operación de incremento.
Para ello hacemos uso de restricciones de tipo:
inc' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
inc' = ask >>= modify . (flip inc) >> get >>= tell . (:[]) . cValue
Aquí llegamos a una solución que funcionará para cualquier mónada que cumpla con las restricciones anteriores. La función de cálculo se define así con el tipo:
mComputation' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
ya que en su cuerpo hacemos uso de inc '.
Podríamos ejecutar este cálculo, en el REPL de ghci
por ejemplo, de la siguiente manera:
runState ( runReaderT ( runWriterT mComputation' ) 15 ) (MkCounter 0)