145 votes

Lecteur Monad But

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.

201voto

Philip JF Points 17248

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

  1. Quelle est la monade faire? Quelles opérations est-il équipé? De quoi est-il bon?
  2. 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!

64voto

Luis Casillas Points 11718

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' VPs 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 Readers. Functor = faire une Reader qui modifie la sortie d'un autre Reader; Applicative = connecter deux Readers à 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.

28voto

Dmitry Bespalov Points 715

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.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X