Haskell Language
Monad Transformers
Recherche…
Un compteur monadique
Un exemple sur la façon de composer le lecteur, l’écrivain et la monade d’état en utilisant des transformateurs monad. Le code source se trouve dans ce référentiel
Nous voulons implémenter un compteur qui incrémente sa valeur par une constante donnée.
Nous commençons par définir certains types et fonctions:
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)
Supposons que nous voulions effectuer le calcul suivant en utilisant le compteur:
- mettre le compteur à 0
- définir la constante d'incrément à 3
- incrémenter le compteur 3 fois
- définir la constante d'incrément à 5
- incrémenter le compteur 2 fois
La monade d'état fournit des abstractions pour faire passer l'état. Nous pouvons utiliser la monade d'état et définir notre fonction d'incrément comme un transformateur d'état.
-- | CounterS is a monad.
type CounterS = State Counter
-- | Increment the counter by 'n' units.
incS :: Int-> CounterS ()
incS n = modify (\c -> inc c n)
Cela nous permet déjà d'exprimer un calcul de manière plus claire et succincte:
-- | The computation we want to run, with the state monad.
mComputationS :: CounterS ()
mComputationS = do
incS 3
incS 3
incS 3
incS 5
incS 5
Mais il faut encore passer l'incrément constant à chaque invocation. Nous aimerions éviter cela.
Ajouter un environnement
Le lecteur monad offre un moyen pratique de faire passer un environnement. Cette monade est utilisée dans la programmation fonctionnelle pour réaliser ce que l'on appelle l' injection de dépendance dans le monde OO.
Dans sa version la plus simple, le monad du lecteur nécessite deux types:
le type de la valeur en cours de lecture (c.-à-d. notre environnement,
r
ci-dessous),la valeur renvoyée par le lecteur monad (
a
cia
dessous).Lecteur ra
Cependant, nous devons également utiliser la monade d'état. Il faut donc utiliser le transformateur ReaderT
:
newtype ReaderT r m a :: * -> (* -> *) -> * -> *
En utilisant ReaderT
, nous pouvons définir notre compteur avec l'environnement et indiquer comme suit:
type CounterRS = ReaderT Int CounterS
Nous définissons une fonction incR
qui prend la constante d'incrément de l'environnement (en utilisant ask
), et pour définir notre fonction d'incrémentation en fonction de notre monade CounterS
nous utilisons la fonction lift
(qui appartient à la classe du transformateur monad ).
-- | Increment the counter by the amount of units specified by the environment.
incR :: CounterRS ()
incR = ask >>= lift . incS
En utilisant le lecteur monad on peut définir notre calcul comme suit:
-- | 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
Les exigences ont changé: nous avons besoin de journalisation!
Supposons maintenant que nous souhaitons ajouter la journalisation à notre calcul, afin que nous puissions voir l'évolution de notre compteur dans le temps.
Nous avons également une monade pour effectuer cette tâche, l' écrivain monad . Comme avec le lecteur monad, puisque nous les composons, nous devons utiliser le transformateur monad du lecteur:
newtype WriterT w m a :: * -> (* -> *) -> * -> *
Ici , w
représente le type de sortie pour accumuler (qui doit être un monoid, qui nous permettent d'accumuler cette valeur), m
est la monade intérieure, et a
type de calcul.
Nous pouvons alors définir notre compteur avec logging, environment et state comme suit:
type CounterWRS = WriterT [Int] CounterRS
Et en utilisant lift
nous pouvons définir la version de la fonction d'incrément qui enregistre la valeur du compteur après chaque incrément:
incW :: CounterWRS ()
incW = lift incR >> get >>= tell . (:[]) . cValue
Maintenant, le calcul qui contient la journalisation peut être écrit comme suit:
mComputationWRS :: CounterWRS ()
mComputationWRS = do
local (const 3) $ do
incW
incW
incW
local (const 5) $ do
incW
incW
Faire tout en une fois
Cet exemple a pour but de montrer les transformateurs monad au travail. Cependant, nous pouvons obtenir le même effet en composant tous les aspects (environnement, état et journalisation) en une seule opération d'incrémentation.
Pour ce faire, nous utilisons des contraintes de type:
inc' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
inc' = ask >>= modify . (flip inc) >> get >>= tell . (:[]) . cValue
Nous arrivons ici à une solution qui fonctionnera pour n'importe quelle monade répondant aux contraintes ci-dessus. La fonction de calcul est définie avec le type:
mComputation' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
puisque dans son corps, nous utilisons inc.
Nous pourrions exécuter ce calcul, dans la ghci
REPL par exemple, comme suit:
runState ( runReaderT ( runWriterT mComputation' ) 15 ) (MkCounter 0)