Haskell Language
Mónada del estado
Buscar..
Introducción
State sa
que representa un cálculo que lleva y potencialmente modifica un estado de tipo s
y produce un resultado de tipo a
, pero el término "mónada de estado" generalmente se refiere a cualquier mónada que lleva un estado. El paquete mtl
y transformers
proporciona implementaciones generales de mónadas estatales.
Observaciones
Los recién llegados a Haskell a menudo se alejan de la mónada State
y la tratan como un tabú, ya que el beneficio declarado de la programación funcional es evitar el estado, así que, ¿no lo pierde cuando usa State
? Una vista más matizada es que:
- El estado puede ser útil en pequeñas dosis controladas ;
- El tipo de
State
proporciona la capacidad de controlar la dosis con mucha precisión.
Las razones son que si tienes action :: State sa
, esto te dice que:
-
action
es especial porque depende de un estado; - El estado tiene el tipo
s
, por lo que laaction
no puede verse influenciada por ningún valor antiguo en su programa; solo se puede acceder a unas
o algún valor desde algunoss
; - El
runState :: State sa -> s -> (a, s)
pone una "barrera" alrededor de la acción con estado, de modo que su efectividad no se puede observar desde fuera de esa barrera.
Por lo tanto, este es un buen conjunto de criterios para utilizar el State
en un escenario particular. Quiere ver que su código está minimizando el alcance del estado , tanto al elegir un tipo estrecho para s
como al poner runState
más cerca posible al "fondo" (para que sus acciones puedan verse influidas por tan pocas cosas como posible.
Numerar los nodos de un árbol con un contador.
Tenemos un tipo de datos de árbol como este:
data Tree a = Tree a [Tree a] deriving Show
Y deseamos escribir una función que asigne un número a cada nodo del árbol, desde un contador incremental:
tag :: Tree a -> Tree (a, Int)
El largo camino
Primero lo haremos a lo largo, ya que ilustra muy bien la mecánica de bajo nivel de la mónada State
.
import Control.Monad.State
-- Function that numbers the nodes of a `Tree`.
tag :: Tree a -> Tree (a, Int)
tag tree =
-- tagStep is where the action happens. This just gets the ball
-- rolling, with `0` as the initial counter value.
evalState (tagStep tree) 0
-- This is one monadic "step" of the calculation. It assumes that
-- it has access to the current counter value implicitly.
tagStep :: Tree a -> State Int (Tree (a, Int))
tagStep (Tree a subtrees) = do
-- The `get :: State s s` action accesses the implicit state
-- parameter of the State monad. Here we bind that value to
-- the variable `counter`.
counter <- get
-- The `put :: s -> State s ()` sets the implicit state parameter
-- of the `State` monad. The next `get` that we execute will see
-- the value of `counter + 1` (assuming no other puts in between).
put (counter + 1)
-- Recurse into the subtrees. `mapM` is a utility function
-- for executing a monadic actions (like `tagStep`) on a list of
-- elements, and producing the list of results. Each execution of
-- `tagStep` will be executed with the counter value that resulted
-- from the previous list element's execution.
subtrees' <- mapM tagStep subtrees
return $ Tree (a, counter) subtrees'
Refactorización
Dividir el contador en una acción postIncrement
La parte en la que nos encontramos get
ting el contador actual y luego put
ting contador + 1 se puede separar en una postIncrement
acción, similar a lo que muchos lenguajes de tipo C proporcionan:
postIncrement :: Enum s => State s s
postIncrement = do
result <- get
modify succ
return result
Divide la caminata del árbol en una función de orden superior.
La lógica de la caminata del árbol se puede dividir en su propia función, como esta:
mapTreeM :: Monad m => (a -> m b) -> Tree a -> m (Tree b)
mapTreeM action (Tree a subtrees) = do
a' <- action a
subtrees' <- mapM (mapTreeM action) subtrees
return $ Tree a' subtrees'
Con esto y la función postIncrement
podemos reescribir tagStep
:
tagStep :: Tree a -> State Int (Tree (a, Int))
tagStep = mapTreeM step
where step :: a -> State Int (a, Int)
step a = do
counter <- postIncrement
return (a, counter)
Usa la clase Traversable
La solución mapTreeM
anterior se puede reescribir fácilmente en una instancia de la clase Traversable
:
instance Traversable Tree where
traverse action (Tree a subtrees) =
Tree <$> action a <*> traverse action subtrees
Tenga en cuenta que esto nos obligó a utilizar Applicative
(el operador <*>
) en lugar de Monad
.
Con eso, ahora podemos escribir tag
como un profesional:
tag :: Traversable t => t a -> t (a, Int)
tag init t = evalState (traverse step t) 0
where step a = do tag <- postIncrement
return (a, tag)
¡Tenga en cuenta que esto funciona para cualquier tipo de Traversable
, no solo para nuestro tipo de Tree
!
Deshacerse de la Traversable
desplazamiento de Traversable
GHC tiene una extensión DeriveTraversable
que elimina la necesidad de escribir la instancia anterior:
{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}
data Tree a = Tree a [Tree a]
deriving (Show, Functor, Foldable, Traversable)