Was ist der Zweck der Lesermonade?

122

Die Lesermonade ist so komplex und scheint nutzlos zu sein. In einer imperativen Sprache wie Java oder C ++ gibt es kein gleichwertiges Konzept für die Lesermonade, wenn ich mich nicht irre.

Können Sie mir ein einfaches Beispiel geben und dies ein wenig klären?

chipbk10
quelle
21
Sie verwenden die Reader-Monade, wenn Sie gelegentlich einige Werte aus einer (nicht veränderbaren) Umgebung lesen möchten, diese Umgebung jedoch nicht explizit weitergeben möchten. In Java oder C ++ würden Sie globale Variablen verwenden (obwohl dies nicht genau dasselbe ist).
Daniel Fischer
5
@ Daniel: Das klingt sehr nach einer Antwort
SingleNegationElimination
@TokenMacGuy Zu kurz für eine Antwort, und es ist jetzt zu spät für mich, mir etwas länger auszudenken. Wenn es sonst niemand tut, werde ich es tun, nachdem ich geschlafen habe.
Daniel Fischer
8
In Java oder C ++ entspricht die Reader-Monade den Konfigurationsparametern, die an ein Objekt in seinem Konstruktor übergeben werden und die während der Lebensdauer des Objekts niemals geändert werden. In Clojure ähnelt es einer Variablen mit dynamischem Gültigkeitsbereich, mit der das Verhalten einer Funktion parametrisiert wird, ohne dass sie explizit als Parameter übergeben werden muss.
Danidiaz

Antworten:

169

Hab keine Angst! Die Lesermonade ist eigentlich nicht so kompliziert und hat ein wirklich einfach zu bedienendes Dienstprogramm.

Es gibt zwei Möglichkeiten, sich einer Monade zu nähern: Wir können fragen

  1. Was bedeutet die Monade tun ? Mit welchen Operationen ist es ausgestattet? Wozu ist es gut?
  2. Wie wird die Monade umgesetzt? Woher kommt es?

Vom ersten Ansatz an ist die Lesermonade ein abstrakter Typ

data Reader env a

so dass

-- 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

Wie nutzen wir das? Nun, die Lesermonade ist gut geeignet, um (implizite) Konfigurationsinformationen durch eine Berechnung zu leiten.

Jedes Mal, wenn Sie eine "Konstante" in einer Berechnung haben, die Sie an verschiedenen Punkten benötigen, aber wirklich möchten, dass Sie dieselbe Berechnung mit unterschiedlichen Werten durchführen können, sollten Sie eine Lesermonade verwenden.

Lesermonaden werden auch verwendet, um das zu tun, was die OO-Leute Abhängigkeitsinjektion nennen . Beispielsweise wird der Negamax- Algorithmus häufig (in hochoptimierten Formen) verwendet, um den Wert einer Position in einem Zwei-Spieler-Spiel zu berechnen. Dem Algorithmus selbst ist es jedoch egal, welches Spiel Sie spielen, außer dass Sie in der Lage sein müssen, die "nächsten" Positionen im Spiel zu bestimmen, und Sie müssen in der Lage sein zu erkennen, ob die aktuelle Position eine Siegposition ist.

 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

Dies funktioniert dann mit jedem endlichen, deterministischen Zwei-Spieler-Spiel.

Dieses Muster ist auch für Dinge nützlich, bei denen es sich nicht wirklich um Abhängigkeitsinjektion handelt. Angenommen, Sie arbeiten im Finanzbereich, dann entwerfen Sie möglicherweise eine komplizierte Logik für die Preisgestaltung eines Vermögenswerts (z. B. ein Derivat), die alle gut und schön ist und auf stinkende Monaden verzichten kann. Dann ändern Sie Ihr Programm, um mit mehreren Währungen umzugehen. Sie müssen in der Lage sein, im laufenden Betrieb zwischen Währungen umzurechnen. Ihr erster Versuch besteht darin, eine Funktion der obersten Ebene zu definieren

type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict

Spotpreise zu bekommen. Sie können dieses Wörterbuch dann in Ihrem Code aufrufen ... aber warten Sie! Das wird nicht funktionieren! Das Währungswörterbuch ist unveränderlich und muss daher nicht nur für die Lebensdauer Ihres Programms, sondern ab dem Zeitpunkt seiner Kompilierung gleich sein ! Also, was machst du? Nun, eine Option wäre die Verwendung der Reader-Monade:

 computePrice :: Reader CurrencyDict Dollars
 computePrice
    = do currencyDict <- ask
      --insert computation here

Der vielleicht klassischste Anwendungsfall ist die Implementierung von Dolmetschern. Bevor wir uns das ansehen, müssen wir jedoch eine andere Funktion einführen

 local :: (env -> env) -> Reader env a -> Reader env a

Okay, Haskell und andere funktionale Sprachen basieren auf dem Lambda-Kalkül . Lambda-Kalkül hat eine Syntax, die aussieht

 data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)

und wir wollen einen Evaluator für diese Sprache schreiben. Zu diesem Zweck müssen wir eine Umgebung im Auge behalten, bei der es sich um eine Liste von Bindungen handelt, die mit Begriffen verknüpft sind (tatsächlich handelt es sich um Abschlüsse, da wir statisches Scoping durchführen möchten).

 newtype Env = Env ([(String, Closure)])
 type Closure = (Term, Env)

Wenn wir fertig sind, sollten wir einen Wert (oder einen Fehler) herausholen:

 data Value = Lam String Closure | Failure String

Schreiben wir also den Dolmetscher:

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!

Schließlich können wir es verwenden, indem wir eine triviale Umgebung übergeben:

interp :: Term -> Value
interp term = runReader (interp' term) (Env [])

Und das ist alles. Ein voll funktionsfähiger Interpreter für den Lambda-Kalkül.


Die andere Möglichkeit, darüber nachzudenken, besteht darin, zu fragen: Wie wird es implementiert? Die Antwort ist, dass die Lesermonade tatsächlich eine der einfachsten und elegantesten aller Monaden ist.

newtype Reader env a = Reader {runReader :: env -> a}

Reader ist nur ein ausgefallener Name für Funktionen! Wir haben bereits definiert runReader, was ist also mit den anderen Teilen der API? Nun, jeder Monadist auch ein Functor:

instance Functor (Reader env) where
   fmap f (Reader g) = Reader $ f . g

Nun, um eine Monade zu bekommen:

instance Monad (Reader env) where
   return x = Reader (\_ -> x)
   (Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x

das ist nicht so beängstigend. askist wirklich einfach:

ask = Reader $ \x -> x

während localist nicht so schlimm:

local f (Reader g) = Reader $ \x -> runReader g (f x)

Okay, die Lesermonade ist nur eine Funktion. Warum überhaupt Reader? Gute Frage. Eigentlich brauchst du es nicht!

instance Functor ((->) env) where
  fmap = (.)

instance Monad ((->) env) where
  return = const
  f >>= g = \x -> g (f x) x

Diese sind noch einfacher. Was mehr ist, askist nur idund localist nur Funktionszusammensetzung mit der Reihenfolge der Funktionen umgeschaltet!

Philip JF
quelle
6
Sehr interessante Antwort. Ehrlich gesagt, ich habe es viele Male gelesen, wenn ich Monade rezensieren möchte. Über den Nagamax-Algorithmus scheinen übrigens "Werte <- mapM (negieren. Negamax (Farbe negieren)) möglich" nicht korrekt zu sein. Ich weiß, dass der von Ihnen bereitgestellte Code nur dazu dient, die Funktionsweise der Lesermonade zu zeigen. Aber wenn Sie Zeit haben, können Sie den Code des Negamax-Algorithmus korrigieren? Weil es interessant ist, wenn Sie Reader Monad verwenden, um Negamax zu lösen.
Chipbk10
4
Ist Readeralso eine Funktion mit einer bestimmten Implementierung der Monadentypklasse? Es früher zu sagen hätte mir geholfen, ein bisschen weniger verwirrt zu sein. Zuerst habe ich es nicht verstanden. Auf halbem Weg dachte ich: "Oh, es ermöglicht Ihnen, etwas zurückzugeben, das Ihnen das gewünschte Ergebnis liefert, sobald Sie den fehlenden Wert angeben." Ich fand das nützlich, merkte aber plötzlich, dass eine Funktion genau das tut.
Ziggystar
1
Nachdem ich das gelesen habe, verstehe ich das meiste davon. Die localFunktion bedarf jedoch einiger weiterer Erklärungen.
Christophe De Troyer
@Philip Ich habe eine Frage zur Monad-Instanz. Können wir die Bindefunktion nicht als schreiben (Reader f) >>= g = (g (f x))?
Zeronone
@zeronone wo ist x?
Ashish Negi
56

Ich erinnere mich, wie ich verwirrt war, bis ich selbst entdeckte, dass es überall Varianten der Reader-Monade gibt . Wie habe ich es entdeckt? Weil ich immer wieder Code schrieb, der sich als kleine Variation herausstellte.

Zum Beispiel schrieb ich irgendwann einen Code, um mit historischen Werten umzugehen . Werte, die sich im Laufe der Zeit ändern. Ein sehr einfaches Modell hierfür sind Funktionen von Zeitpunkten bis zum Wert zu diesem Zeitpunkt:

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

Die ApplicativeInstanz bedeutet, dass wenn Sie haben employees :: History Day [Person]und customers :: History Day [Person]Sie dies tun können:

-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers

Das heißt, Functorund Applicativeerlauben Sie uns, regelmäßige, nicht historische Funktionen anzupassen, um mit Geschichten zu arbeiten.

Die Monadeninstanz wird am intuitivsten unter Berücksichtigung der Funktion verstanden (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c. Eine Funktion des Typs a -> History t bist eine Funktion , die eine Karten azu einer Vorgeschichte von bWerten; Zum Beispiel könnten Sie getSupervisor :: Person -> History Day Supervisorund haben getVP :: Supervisor -> History Day VP. In der Monad-Instanz für Historygeht es also darum, Funktionen wie diese zu erstellen. Zum Beispiel getSupervisor >=> getVP :: Person -> History Day VPist die Funktion, die für jeden Persondie Geschichte von VPs erhält, die sie hatten.

Nun, diese HistoryMonade ist eigentlich genau das gleiche wie Reader. History t aist wirklich das gleiche wie Reader t a(was das gleiche ist wie t -> a).

Ein weiteres Beispiel: Ich habe kürzlich in Haskell Prototypen für OLAP- Designs erstellt. Eine Idee hier ist die eines "Hyperwürfels", bei dem es sich um eine Abbildung von Schnittpunkten einer Reihe von Dimensionen auf Werte handelt. Jetzt geht das schon wieder los:

newtype Hypercube intersection value = Hypercube { get :: intersection -> value }

Eine übliche Vorgehensweise bei Hyperwürfeln besteht darin, Skalarfunktionen mit mehreren Stellen auf entsprechende Punkte eines Hyperwürfels anzuwenden. Dies können wir erreichen, indem wir eine ApplicativeInstanz definieren für 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)

Ich habe gerade den obigen HistoryCode kopiert und die Namen geändert. Wie Sie sehen, Hypercubeist auch gerecht Reader.

Es geht weiter und weiter. Zum Beispiel laufen Sprachdolmetscher auch darauf hinaus Reader, wenn Sie dieses Modell anwenden:

  • Ausdruck = a Reader
  • Freie Variablen = Verwendungen von ask
  • Evaluierungsumgebung = ReaderAusführungsumgebung.
  • Bindungskonstrukte = local

Eine gute Analogie ist, dass a Reader r aein amit "Löchern" darstellt, die Sie daran hindern, zu wissen, wovon awir sprechen. Sie können eine tatsächliche nur erhalten, awenn Sie eine an liefern r, um die Löcher zu füllen. Es gibt Unmengen solcher Dinge. In den obigen Beispielen ist ein "Verlauf" ein Wert, der erst berechnet werden kann, wenn Sie eine Zeit angeben. Ein Hypercube ist ein Wert, der erst berechnet werden kann, wenn Sie eine Schnittmenge angeben, und ein Sprachausdruck ist ein Wert, der dies kann wird erst berechnet, wenn Sie die Werte der Variablen angeben. Es gibt Ihnen auch eine Intuition darüber, warum Reader r adas gleiche ist wie r -> a, weil eine solche Funktion auch intuitiv ein afehlendes ist r.

Das Functorund Applicativeund die MonadInstanzen von Readersind daher eine sehr nützliche Verallgemeinerung für Fälle, in denen Sie etwas der Art "ein afehlendes r" modellieren und es Ihnen ermöglichen, diese "unvollständigen" Objekte so zu behandeln, als wären sie vollständig.

Noch ein anderer Weg , um die gleiche Sache zu sagen: a Reader r aist etwas , das verbraucht rund produziert a, und die Functor, Applicativeund MonadInstanzen sind Grundmuster für mit Arbeits Readers. Functor= mache ein Reader, das die Ausgabe eines anderen ändert Reader; Applicative= Verbinde zwei Readers mit demselben Eingang und kombiniere ihre Ausgänge; Monad= Untersuche das Ergebnis von a Readerund verwende es, um ein anderes zu konstruieren Reader. Die localund withReaderFunktionen = machen eine Reader, die die Eingabe in eine andere ändert Reader.

Luis Casillas
quelle
5
Gute Antwort. Sie können auch die Verwendung GeneralizedNewtypeDerivingErweiterung herzuleiten Functor, Applicative, Monadetc. für newtypes auf der Grundlage ihrer zugrunde liegenden Typen.
Rein Henrichs
20

In Java oder C ++ können Sie problemlos von überall auf jede Variable zugreifen. Probleme treten auf, wenn Ihr Code Multithreading wird.

In Haskell haben Sie nur zwei Möglichkeiten, den Wert von einer Funktion an eine andere zu übergeben:

  • Sie übergeben den Wert über einen der Eingabeparameter der aufrufbaren Funktion. Nachteile sind: 1) Sie können nicht ALLE Variablen auf diese Weise übergeben - die Liste der Eingabeparameter ist einfach umwerfend. 2) In Folge von Funktionsaufrufen : fn1 -> fn2 -> fn3, benötigt die Funktion fn2möglicherweise keinen Parameter, von dem Sie fn1an übergeben fn3.
  • Sie übergeben den Wert im Umfang einer Monade. Nachteil ist: Man muss genau verstehen, was Monadenkonzeption ist. Das Weitergeben der Werte ist nur eine von vielen Anwendungen, in denen Sie die Monaden verwenden können. Tatsächlich ist die Monadenkonzeption unglaublich mächtig. Sei nicht verärgert, wenn du nicht sofort einen Einblick bekommst. Versuchen Sie es einfach weiter und lesen Sie verschiedene Tutorials. Das Wissen, das Sie erhalten, wird sich auszahlen.

Die Reader-Monade übergibt nur die Daten, die Sie zwischen Funktionen teilen möchten. Funktionen können diese Daten lesen, aber nicht ändern. Das ist alles, was die Reader-Monade ausmacht. Na ja, fast alle. Es gibt auch eine Reihe von Funktionen wie local, aber zum ersten Mal können Sie nur bleiben asks.

Dmitry Bespalov
quelle
3
Ein weiterer Nachteil der Verwendung von Monaden zur impliziten Weitergabe von Daten besteht darin, dass es sehr leicht ist, viel Code im Imperativ-Stil in doNotation zu schreiben, was besser wäre, wenn man ihn in eine reine Funktion umgestalten würde.
Benjamin Hodgson
4
@BenjaminHodgson Das Schreiben von 'imperativ aussehendem' Code mit Monaden in do-Notation bedeutet nicht unbedingt das Schreiben von nebenwirkendem (unreinem) Code. Tatsächlich ist seitlich wirksamer Code in Haskell möglicherweise nur innerhalb der E / A-Monade möglich.
Dmitry Bespalov
Wenn die andere Funktion durch eine whereKlausel an die eine angehängt ist, wird sie dann als dritte Möglichkeit zum Übergeben von Variablen akzeptiert?
Elmex80s