Haskell Language
Etat Monad
Recherche…
Introduction
State sa
qui représente un calcul qui porte et modifie potentiellement un état de type s
et produit un résultat de type a
, mais le terme "état monade" peut généralement se référer à toute monade qui porte un état. Le mtl
et transformers
fournit des implémentations générales de monades d'état.
Remarques
Les nouveaux arrivants à Haskell ont souvent peur de la monade de l' State
et la traitent comme un tabou. Comme le prétendu avantage de la programmation fonctionnelle est l'évitement de l'état, ne perdez-vous pas cela lorsque vous utilisez State
? Une vue plus nuancée est que:
- L'état peut être utile en petites doses contrôlées ;
- Le type d'
State
permet de contrôler la dose très précisément.
Les raisons étant que si vous avez l' action :: State sa
, cela vous dit que:
-
action
est spéciale car elle dépend d'un état; - L'état a le type
s
, donc l'action
ne peut pas être influencée par une ancienne valeur de votre programme - seulement une valeurs
ou une valeur accessible depuis certainss
; - Le
runState :: State sa -> s -> (a, s)
place une "barrière" autour de l'action avec état, de sorte que son efficacité ne peut pas être observée en dehors de cette barrière.
Il s’agit donc d’un bon ensemble de critères pour savoir s’il faut utiliser l’ State
dans un scénario particulier. Vous voulez voir que votre code minimise la portée de l’état , à la fois en choisissant un type étroit pour s
et en plaçant runState
plus près possible du "bas" (afin que vos actions puissent être aussi peu influencées que possible). possible.
Numérotation des nœuds d'un arbre avec un compteur
Nous avons un type de données arborescent comme ceci:
data Tree a = Tree a [Tree a] deriving Show
Et nous souhaitons écrire une fonction qui attribue un numéro à chaque nœud de l’arbre, à partir d’un compteur incrémenté:
tag :: Tree a -> Tree (a, Int)
Le long chemin
D' abord , nous le ferons long chemin, car il illustre l' State
tout à fait bien la mécanique à faible niveau de monades.
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'
Refactoring
Diviser le compteur en une action postIncrement
Le bit où nous get
le compteur actuel puis put
compteur + 1 peut être divisé en une action postIncrement
, similaire à ce que de nombreux langages de style C fournissent:
postIncrement :: Enum s => State s s
postIncrement = do
result <- get
modify succ
return result
Diviser l'arbre en une fonction d'ordre supérieur
La logique de parcours d'arbre peut être divisée en sa propre fonction, comme ceci:
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'
Avec ceci et la fonction postIncrement
, nous pouvons réécrire 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)
Utilisez la classe Traversable
La solution mapTreeM
ci-dessus peut être facilement réécrite dans une instance de la classe Traversable
:
instance Traversable Tree where
traverse action (Tree a subtrees) =
Tree <$> action a <*> traverse action subtrees
Notez que cela nous a obligé à utiliser Applicative
(l'opérateur <*>
) au lieu de Monad
.
Avec ça, maintenant on peut écrire tag
comme un pro:
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)
Notez que cela fonctionne pour n'importe quel type Traversable
, pas seulement notre type d' Tree
!
Se débarrasser du passe-partout Traversable
GHC a une extension DeriveTraversable
qui élimine le besoin d'écrire l'instance ci-dessus:
{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}
data Tree a = Tree a [Tree a]
deriving (Show, Functor, Foldable, Traversable)