N'aie pas peur! La monade de lecture n'est en fait pas si compliquée et possède une réelle utilité facile à utiliser.
Il y a deux manières d'aborder une monade: on peut demander
- Qu'est-ce que la monade faire ? De quelles opérations est-il équipé? À quoi ça sert?
- Comment la monade est-elle mise en œuvre? D'où vient-il?
Dès la première approche, la monade du lecteur est un type abstrait
data Reader env a
tel que
-- Reader is a monad
instance Monad (Reader env)
-- and we have a function to get its environment
ask :: Reader env env
-- finally, we can run a Reader
runReader :: Reader env a -> env -> a
Alors, comment utilisons-nous cela? Eh bien, la monade de lecture est bonne pour passer des informations de configuration (implicites) à travers un calcul.
Chaque fois que vous avez une "constante" dans un calcul dont vous avez besoin à différents moments, mais que vous souhaitez vraiment pouvoir effectuer le même calcul avec des valeurs différentes, vous devez utiliser une monade de lecture.
Les monades de lecteur sont également utilisées pour faire ce que les personnes OO appellent l' injection de dépendances . Par exemple, l' algorithme negamax est fréquemment utilisé (sous des formes hautement optimisées) pour calculer la valeur d'une position dans une partie à deux joueurs. L'algorithme lui-même ne se soucie pas du jeu auquel vous jouez, sauf que vous devez être en mesure de déterminer quelles sont les "prochaines" positions dans le jeu, et vous devez être capable de dire si la position actuelle est une position de victoire.
import Control.Monad.Reader
data GameState = NotOver | FirstPlayerWin | SecondPlayerWin | Tie
data Game position
= Game {
getNext :: position -> [position],
getState :: position -> GameState
}
getNext' :: position -> Reader (Game position) [position]
getNext' position
= do game <- ask
return $ getNext game position
getState' :: position -> Reader (Game position) GameState
getState' position
= do game <- ask
return $ getState game position
negamax :: Double -> position -> Reader (Game position) Double
negamax color position
= do state <- getState' position
case state of
FirstPlayerWin -> return color
SecondPlayerWin -> return $ negate color
Tie -> return 0
NotOver -> do possible <- getNext' position
values <- mapM ((liftM negate) . negamax (negate color)) possible
return $ maximum values
Cela fonctionnera ensuite avec n'importe quelle partie à deux joueurs finie et déterministe.
Ce modèle est utile même pour les choses qui ne sont pas vraiment une injection de dépendance. Supposons que vous travaillez dans la finance, vous pouvez concevoir une logique compliquée pour évaluer un actif (un dérivé, par exemple), ce qui est très bien et vous pouvez vous passer de monades puantes. Mais ensuite, vous modifiez votre programme pour traiter plusieurs devises. Vous devez être capable de convertir entre les devises à la volée. Votre première tentative consiste à définir une fonction de niveau supérieur
type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
pour obtenir des prix au comptant. Vous pouvez alors appeler ce dictionnaire dans votre code .... mais attendez! Cela ne fonctionnera pas! Le dictionnaire de devises est immuable et doit donc être le même non seulement pour la durée de vie de votre programme, mais aussi à partir du moment où il est compilé ! Donc que fais-tu? Eh bien, une option serait d'utiliser la monade Reader:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
Le cas d'utilisation le plus classique est peut-être celui de l'implémentation d'interprètes. Mais, avant de regarder cela, nous devons introduire une autre fonction
local :: (env -> env) -> Reader env a -> Reader env a
D'accord, donc Haskell et d'autres langages fonctionnels sont basés sur le calcul lambda . Le calcul Lambda a une syntaxe qui ressemble à
data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
et nous voulons écrire un évaluateur pour cette langue. Pour ce faire, nous devrons garder une trace d'un environnement, qui est une liste de liaisons associées à des termes (en fait, ce seront des fermetures parce que nous voulons faire une portée statique).
newtype Env = Env ([(String, Closure)])
type Closure = (Term, Env)
Lorsque nous avons terminé, nous devrions sortir une valeur (ou une erreur):
data Value = Lam String Closure | Failure String
Alors, écrivons l'interpréteur:
interp' :: Term -> Reader Env Value
--when we have a lambda term, we can just return it
interp' (Lambda nv t)
= do env <- ask
return $ Lam nv (t, env)
--when we run into a value, we look it up in the environment
interp' (Var v)
= do (Env env) <- ask
case lookup (show v) env of
-- if it is not in the environment we have a problem
Nothing -> return . Failure $ "unbound variable: " ++ (show v)
-- if it is in the environment, then we should interpret it
Just (term, env) -> local (const env) $ interp' term
--the complicated case is an application
interp' (Apply t1 t2)
= do v1 <- interp' t1
case v1 of
Failure s -> return (Failure s)
Lam nv clos -> local (\(Env ls) -> Env ((nv, clos) : ls)) $ interp' t2
--I guess not that complicated!
Enfin, nous pouvons l'utiliser en passant un environnement trivial:
interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
Et c'est tout. Un interpréteur entièrement fonctionnel pour le calcul lambda.
L'autre façon d'y penser est de se demander: comment est-il mis en œuvre? La réponse est que la monade du lecteur est en fait l'une des monades les plus simples et les plus élégantes.
newtype Reader env a = Reader {runReader :: env -> a}
Reader est juste un nom sophistiqué pour les fonctions! Nous avons déjà défini runReader
alors qu'en est-il des autres parties de l'API? Eh bien, tout Monad
est aussi un Functor
:
instance Functor (Reader env) where
fmap f (Reader g) = Reader $ f . g
Maintenant, pour obtenir une monade:
instance Monad (Reader env) where
return x = Reader (\_ -> x)
(Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
ce qui n'est pas si effrayant. ask
c'est vraiment simple:
ask = Reader $ \x -> x
alors que ce local
n'est pas si mal:
local f (Reader g) = Reader $ \x -> runReader g (f x)
D'accord, donc la monade du lecteur n'est qu'une fonction. Pourquoi avoir Reader du tout? Bonne question. En fait, vous n'en avez pas besoin!
instance Functor ((->) env) where
fmap = (.)
instance Monad ((->) env) where
return = const
f >>= g = \x -> g (f x) x
Celles-ci sont encore plus simples. De plus, ask
c'est juste id
et local
c'est juste une composition de fonction avec l'ordre des fonctions commuté!