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)


Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow