Je suis novice à Haskell. J'ai lu le Reader Monad plusieurs fois, mais je ne comprends toujours pas à quoi sert le Reader Monad. Le lecteur Monad est si complexe et semble être inutile. Dans un langage impératif comme Java ou C ++, il n'y a pas de terme équivalent pour monad de lecteur (si j'ai raison). Pouvez-vous me donner un exemple simple et me clarifier un peu. Désolé pour mon ignorance.
Réponses
Trop de publicités?N'ayez pas peur! Le lecteur monade est en fait pas si compliqué, et est très facile à utiliser utilitaire.
Il y a deux façons d'aborder une monade: nous pouvons nous demander
- Quelle est la monade faire? Quelles opérations est-il équipé? De quoi est-il bon?
- Comment est la monade mis en œuvre? Où apparaît-il?
À partir de la première approche, le lecteur monade est un certain type abstrait
data Reader env a
tels 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 faisons-nous cela? Ainsi, le lecteur monade est bon pour passer (implicite) des informations de configuration par le biais d'un calcul.
Toutes les fois que vous avez une "constante" dans un calcul dont vous avez besoin à divers points, mais vraiment vous voulez être en mesure d'effectuer le même calcul avec des valeurs différentes, alors vous devez utiliser un lecteur de monade.
Lecteur de monades sont également utilisés pour faire ce que l'OO les gens l'appellent l'injection de dépendance. Par exemple, le negamax algorithme est utilisé fréquemment (dans hautement optimisé formes) pour calculer la valeur d'une position dans une partie à deux joueurs. L'algorithme lui-même mais ne se soucient pas de ce jeu que vous jouez, sauf que vous avez besoin pour être en mesure de déterminer ce que le "prochain" des postes sont dans le jeu, et vous devez être en mesure de dire si la position actuelle est une victoire de la position.
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
Ce sera ensuite travailler avec tout fini, déterministe, jeu à deux.
Ce modèle est utile même pour des choses qui ne sont pas vraiment d'injection de dépendance. Supposons que vous travaillez dans la finance, vous pouvez concevoir certains de logique compliquée pour le prix d'un actif (un dérivé dire), ce qui est bien et bon, et vous pouvez le faire sans puant monades. Mais ensuite, vous modifiez votre programme pour face à de multiples devises. Vous devez être en mesure de convertir entre les monnaies à la volée. Votre première tentative est de définir un niveau supérieur de la fonction
type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
pour obtenir le prix spot. Vous pouvez ensuite appeler ce dictionnaire dans votre code....mais attendez! Cela ne marchera pas! La monnaie dictionnaire est immuable et donc, doit être le même, non seulement pour la durée de vie de votre programme, mais à partir du moment qu'il obtient compilé! Alors que faites-vous? Une option serait d'utiliser le Lecteur monade:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
Peut-être le plus classique de cas d'utilisation est dans la mise en œuvre des interprètes. Mais, avant de nous intéresser à cela, nous avons besoin d'introduire une autre fonction
local :: (env -> env) -> Reader env a -> Reader env a
Ok, donc Haskell et autres langages fonctionnels sont basés sur le lambda calcul. Lambda calcul 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 aurons besoin de garder une trace d'un environnement, qui est une liste de raccourcis associés à des termes (en fait, il sera fermetures parce que nous voulons faire statique de la portée).
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
Donc, permet d'écrire l'interprète:
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, than 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 trivial de l'environnement:
interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
Et c'est tout. Entièrement fonctionnelle d'interprète pour le lambda calcul.
Donc, l'autre façon de penser est de se demander: comment est-il mis en œuvre? Eh bien la réponse est que le lecteur monade est en fait l'un des plus simple et la plus élégante de toutes les monades.
newtype Reader env a = Reader {runReader :: env -> a}
Reader est un nom de fantaisie pour les fonctions! Nous avons déjà défini runReader
alors que sur les autres parties de l'API? 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 très simple:
ask = Reader $ \x -> x
alors qu' local
n'est pas si mal.
local f (Reader g) = Reader $ \x -> runReader g (f x)
Ok, donc le lecteur monade est juste une fonction. Pourquoi avez-Lecteur? 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
Ceux-ci sont même plus simple. Ce qui est plus, en ask
est juste id
et local
est seulement fonction de la composition dans l'ordre!
Je me souviens avoir été surpris que vous étiez, jusqu'à ce que j'ai découvert sur mon propre que les variantes du Lecteur monade sont partout. Comment ai-je découvrir? Parce que j'ai continué à écrire du code qui s'est avéré être de petites variations.
Par exemple, à un moment j'étais à écrire du code pour gérer historique des valeurs; les valeurs qui changent au fil du temps. Un modèle très simple de ce qui est des fonctions à partir de points de temps de la valeur à ce point dans le temps:
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' Applicative
exemple signifie que si vous avez employees :: History Day [Person]
et customers :: History Day [Person]
vous pouvez faire:
-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers
I. e., Functor
et Applicative
nous permettent d'adapter régulièrement, non-historique des fonctions pour travailler avec des histoires.
La monade instance est le plus intuitivement compris en tenant compte de 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 associe un a
à une histoire de l' b
valeurs; par exemple, vous pourriez avoir getSupervisor :: Person -> History Day Supervisor
, et getVP :: Supervisor -> History Day VP
. Si la Monade exemple pour History
est à propos de la composition de ces fonctions; par exemple, getSupervisor >=> getVP :: Person -> History Day VP
est la fonction qui obtient, pour tout Person
, l'histoire de l' VP
s qu'ils ont eu.
Eh bien, c' History
monade est en fait exactement la même chose que Reader
. History t a
est vraiment la même que Reader t a
(qui est le même que t -> a
).
Un autre exemple: j'ai été prototypage OLAP dessins en Haskell récemment. Une idée ici est celle d'un "hypercube", qui est une correspondance entre les intersections d'un ensemble de dimensions de valeurs. Ici, nous allons à nouveau:
newtype Hypercube intersection value = Hypercube { get :: intersection -> value }
Une commune de l'opération sur les hypercubes consiste à appliquer un multi-place des fonctions scalaires de points correspondants d'un hypercube. Ce que nous pouvons obtenir par la définition d'un Applicative
exemple 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 copypasted l' History
code ci-dessus et a changé de nom. Comme vous pouvez le dire, Hypercube
est également à seulement Reader
.
Il va sur et sur. Par exemple, les interprètes de la langue aussi bouillir Reader
, lorsque vous appliquez ce modèle:
- Expression = un
Reader
- Variables libres = utilise des
ask
- L'évaluation de l'environnement =
Reader
environnement d'exécution. - La liaison des constructions =
local
Une bonne analogie est qu'un Reader r a
représente un a
"à trous", qui vous empêchent de savoir quel a
nous parlons. Vous ne pouvez obtenir un réel a
une fois que vous fournir un r
pour remplir les trous. Il ya des tonnes de choses comme ça. Dans les exemples ci-dessus, une "histoire" est une valeur qui ne peut pas être calculée jusqu'à ce que vous indiquez un temps, un hypercube est une valeur qui ne peut pas être calculée jusqu'à ce que vous spécifiez une intersection, et une langue d'expression est une valeur qui ne peut pas être calculée jusqu'à ce que vous fournissez les valeurs des variables. Il vous donne aussi une intuition sur pourquoi est - Reader r a
est le même que r -> a
, car une telle fonction est aussi intuitivement une a
manque un r
.
Si l' Functor
, Applicative
et Monad
des occurrences de Reader
sont très utiles pour la généralisation pour les cas où vous êtes à la modélisation de quoi que ce soit de la sorte " a
qui manque un r
," et vous permettent de traiter ces "incomplet" les objets 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 l' Functor
, Applicative
et Monad
instances sont des modèles de base pour travailler avec des Reader
s. Functor
= faire une Reader
qui modifie la sortie d'un autre Reader
; Applicative
= connecter deux Reader
s à la même entrée et de combiner leurs sorties; Monad
= inspecter le résultat d'un Reader
et l'utiliser pour construire un autre Reader
. L' local
et withReader
fonctions = faire une Reader
qui modifie l'entrée à l'autre Reader
.
En Java ou en C++, vous pouvez accéder à toutes les variables à partir de n'importe où sans aucun problème. Problèmes apparaît whey votre code devient multi-thread.
En Haskell vous n'avez que deux façons de transmettre la valeur d'une fonction à l'autre:
- Vous passez la valeur à travers l'un des paramètres d'entrée de la fonction appelable. Les inconvénients sont: 1) vous ne pouvez pas passer TOUTES les variables de cette manière - liste des paramètres d'entrée souffler votre esprit. 2) dans la séquence d'appels de fonction: fn1 -> fn2 -> fn3, la fonction fn2 ne peuvent pas besoin de paramètre que vous vous passer de fn1 à fn3.
- Vous passez la valeur dans le champ d'application de certaines monade. Inconvénient: vous devez obtenir la bonne compréhension de ce que Monade conception est. Transmettre des valeurs autour d'un seul des milliards d'applications où vous pouvez utiliser la Monade. En fait Monade conception est incroyable puissant. Ne pas être dérangé, si vous ne vous êtes pas aperçu à la fois. Essayez juste de garder, et de lire les différents tutoriels. La connaissance que vous aurez payer.
Le Lecteur monade juste de transmettre les données que vous souhaitez partager entre les fonctions. Les fonctions peuvent lire les données, mais ne peut pas le changer. C'est tout ce qui n'Lecteur monade. Enfin, presque tous. Il y a également le nombre de fonctions comme l' local
, mais pour la première fois, vous pouvez coller avec asks
seulement.