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 ci a 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)


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow