La monade du lecteur est si complexe et semble inutile. Dans un langage impératif comme Java ou C++, il n'y a pas de concept équivalent pour la monade de lecture, si je ne me trompe pas.
Pouvez-vous me donner un exemple simple et clarifier un peu cela?
N'ayez pas peur! La monade de lecture n'est en fait pas si compliquée et possède un véritable utilitaire facile à utiliser.
Il y a deux façons d'approcher une monade: on peut demander
De la première approche, la monade de 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 idéale pour transmettre des informations de configuration (implicites) via un calcul.
Chaque fois que vous avez une "constante" dans un calcul dont vous avez besoin à différents points, mais que vous souhaitez vraiment pouvoir effectuer le même calcul avec des valeurs différentes, vous devez alors utiliser une monade de lecture.
Les monades de lecture sont également utilisées pour faire ce que les gens OO appellent injection de dépendance . Par exemple, l'algorithme negamax est fréquemment utilisé (en très formes optimisées) pour calculer la valeur d'une position dans un jeu à deux joueurs. L'algorithme lui-même ne se soucie toutefois 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 savoir 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 alors avec n'importe quel jeu à deux joueurs fini 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 travailliez dans la finance, vous pourriez concevoir une logique compliquée pour la tarification d'un actif (un dérivé disons), ce qui est bien beau et vous pouvez vous passer de monades puantes. Mais ensuite, vous modifiez votre programme pour gérer plusieurs devises. Vous devez pouvoir 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 ensuite appeler ce dictionnaire dans votre code .... mais attendez! Ça ne marchera pas! Le dictionnaire des devises est immuable et doit donc être le même non seulement pour la durée de vie de votre programme, mais à 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 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 lambda calculus . 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 aux 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 obtenir 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 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-elle mise en œuvre? La réponse est que la monade de lecture est en fait l'une des monades les plus simples et les plus élégantes.
newtype Reader env a = Reader {runReader :: env -> a}
Reader n'est qu'un nom de fantaisie pour les fonctions! Nous avons déjà défini runReader
alors qu'en est-il des autres parties de l'API? Eh bien, chaque 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
est vraiment simple:
ask = Reader $ \x -> x
tandis que local
n'est pas si mal.
local f (Reader g) = Reader $ \x -> runReader g (f x)
D'accord, la monade de lecture n'est donc qu'une fonction. Pourquoi avoir Reader? 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
Ce sont encore plus simples. De plus, ask
est juste id
et local
est juste la composition des fonctions avec l'ordre des fonctions changé!
Je me souviens avoir été perplexe comme vous l'étiez, jusqu'à ce que je découvre par moi-même que les variantes de la monade Reader sont partout. Comment l'ai-je découvert? Parce que j'ai continué à écrire du code qui s'est avéré être de petites variations.
Par exemple, à un moment donné, j'écrivais du code pour gérer les valeurs historique; des valeurs qui changent avec le temps. Un modèle très simple de ceci est les fonctions des points de temps à la valeur à ce moment:
import Control.Applicative
-- | A History with timeline type t and value type a.
newtype History t a = History { observe :: t -> a }
instance Functor (History t) where
-- Apply a function to the contents of a historical value
fmap f hist = History (f . observe hist)
instance Applicative (History t) where
-- A "pure" History is one that has the same value at all points in time
pure = History . const
-- This applies a function that changes over time to a value that also
-- changes, by observing both at the same point in time.
ff <*> fx = History $ \t -> (observe ff t) (observe fx t)
instance Monad (History t) where
return = pure
ma >>= f = History $ \t -> observe (f (observe ma t)) t
L'instance Applicative
signifie que si vous avez employees :: History Day [Person]
et customers :: History Day [Person]
tu peux le faire:
-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers
C'est-à-dire que Functor
et Applicative
nous permettent d'adapter des fonctions régulières et non historiques pour travailler avec des historiques.
L'instance monade est plus intuitivement comprise en considérant la fonction (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
. Une fonction de type a -> History t b
est une fonction qui mappe un a
à un historique des valeurs de b
; par exemple, vous pourriez avoir getSupervisor :: Person -> History Day Supervisor
, et getVP :: Supervisor -> History Day VP
. Ainsi, l'instance Monad pour History
concerne la composition de fonctions comme celles-ci; par exemple, getSupervisor >=> getVP :: Person -> History Day VP
est la fonction qui obtient, pour tout Person
, l'historique des VP
qu'ils ont eu.
Eh bien, cette monade History
est en fait exactement identique à Reader
. History t a
est vraiment la même chose que Reader t a
(qui est identique à t -> a
).
Un autre exemple: j'ai fait du prototypage OLAP designs dans Haskell récemment. Une idée ici est celle d'un "hypercube", qui est une correspondance entre les intersections d'un ensemble de dimensions et de valeurs. On y va encore une fois:
newtype Hypercube intersection value = Hypercube { get :: intersection -> value }
Une opération courante sur les hypercubes consiste à appliquer des fonctions scalaires à emplacements multiples aux points correspondants d'un hypercube. Ceci peut être obtenu en définissant une instance de Applicative
pour Hypercube
:
instance Functor (Hypercube intersection) where
fmap f cube = Hypercube (f . get cube)
instance Applicative (Hypercube intersection) where
-- A "pure" Hypercube is one that has the same value at all intersections
pure = Hypercube . const
-- Apply each function in the @ff@ hypercube to its corresponding point
-- in @fx@.
ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)
Je viens de copier le code History
ci-dessus et de changer les noms. Comme vous pouvez le constater, Hypercube
est également juste Reader
.
Et ça continue, encore et encore. Par exemple, les interprètes de langue se résument également à Reader
, lorsque vous appliquez ce modèle:
Reader
ask
Reader
.local
Une bonne analogie est qu'un Reader r a
représente un a
avec des "trous", qui vous empêchent de savoir de quel a
nous parlons. Vous ne pouvez obtenir un a
réel qu'une fois que vous avez fourni un r
pour remplir les trous. Il y a des tonnes de choses comme ça. Dans les exemples ci-dessus, un "historique" est une valeur qui ne peut pas être calculée tant que vous ne spécifiez pas une heure, un hypercube est une valeur qui ne peut pas être calculée tant que vous ne spécifiez pas une intersection, et une expression de langage est une valeur qui peut 't être calculé jusqu'à ce que vous fournissez les valeurs des variables. Il vous donne également une intuition sur la raison pour laquelle Reader r a
est le même que r -> a
, car une telle fonction est aussi intuitivement un a
manquant un r
.
Ainsi, les instances Functor
, Applicative
et Monad
de Reader
sont une généralisation très utile pour les cas où vous modélisez quelque chose du genre "an a
qui manque un r
", et vous permet de traiter ces objets" incomplets "comme s'ils étaient complets.
Encore une autre façon de dire la même chose: un Reader r a
est quelque chose qui consomme r
et produit a
, et les instances Functor
, Applicative
et Monad
sont des modèles de base pour travailler avec Reader
s. Functor
= créer un Reader
qui modifie la sortie d'un autre Reader
; Applicative
= connectez deux Reader
à la même entrée et combinez leurs sorties; Monad
= inspecter le résultat d'un Reader
et l'utiliser pour construire un autre Reader
. Les fonctions local
et withReader
= créent un Reader
qui modifie l'entrée en une autre Reader
.
Dans Java ou C++, vous pouvez accéder à n'importe quelle variable de n'importe où sans aucun problème. Des problèmes apparaissent lorsque votre code devient multi-thread.
Dans Haskell, vous n'avez que deux façons de passer la valeur d'une fonction à une autre:
fn1 -> fn2 -> fn3
, une fonction fn2
peut ne pas avoir besoin d'un paramètre que vous passez de fn1
à fn3
.Le Reader monad passe simplement les données que vous souhaitez partager entre les fonctions. Les fonctions peuvent lire ces données, mais ne peuvent pas les modifier. C'est tout ce que fait la monade Reader. Enfin presque tout. Il existe également un certain nombre de fonctions comme local
, mais pour la première fois, vous pouvez vous en tenir à asks
uniquement.