webentwicklung-frage-antwort-db.com.de

Was ist der Zweck der Lesermonade?

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

Können Sie mir ein einfaches Beispiel geben und dies ein wenig erläutern?

110
chipbk10

Hab keine Angst! Die Leser-Monade ist eigentlich nicht so kompliziert und hat eine wirklich einfach zu bedienende Funktion.

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

  1. Was macht die Monade do? Mit welchen Operationen ist es ausgestattet? Wozu ist es gut?
  2. Wie ist die Monade implementiert? Woher kommt es?

Vom ersten Ansatz an ist die Lesemonade 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? Die Leser-Monade ist gut dafür geeignet, (implizite) Konfigurationsinformationen durch eine Berechnung zu leiten.

Immer wenn Sie eine "Konstante" in einer Berechnung haben, die Sie an verschiedenen Stellen benötigen, aber wirklich dieselbe Berechnung mit unterschiedlichen Werten durchführen möchten, sollten Sie eine Leser-Monade verwenden.

Lesemonaden werden auch verwendet, um das zu tun, was die OO Leute Abhängigkeitsinjektion nennen. Beispielsweise wird der Algorithmus negamax häufig (in hochoptimierten Formen) verwendet, um den Wert einer Position in einem Spiel für zwei Spieler 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 zu erkennen, ob es sich bei der aktuellen Position um eine Siegposition handelt.

 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 Spiel für zwei Spieler.

Dieses Muster ist auch für Dinge nützlich, die keine Abhängigkeitsinjektion sind. Angenommen, Sie arbeiten in der Finanzbranche und entwerfen möglicherweise eine komplizierte Logik für die Preisgestaltung eines Vermögenswerts (beispielsweise ein Derivat). Dies ist alles in Ordnung und gut, und Sie können auf stinkende Monaden verzichten. Aber 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

spot-Preise zu bekommen. Sie können dieses Wörterbuch dann in Ihrem Code aufrufen ... aber warten Sie! Das geht nicht Das Währungswörterbuch ist unveränderlich und muss daher nicht nur für die Laufzeit Ihres Programms gleich sein, sondern ab dem Zeitpunkt, an dem es kompiliert wird ! Also, was machst du? Nun, eine Möglichkeit wäre, die Reader-Monade zu verwenden:

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

Der vielleicht klassischste Anwendungsfall ist die Implementierung von Dolmetschern. Aber bevor wir uns das ansehen, müssen wir 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 möchten einen Bewerter für diese Sprache schreiben. Um dies zu tun, müssen wir eine Umgebung nachverfolgen, die eine Liste der mit Begriffen verbundenen Bindungen ist (tatsächlich handelt es sich um Closures, weil 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) ausgeben:

 data Value = Lam String Closure | Failure String

Schreiben wir also den Interpreter:

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!

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 die Lambda-Rechnung.


Die andere Möglichkeit, darüber nachzudenken, ist die Frage: Wie wird es implementiert? Die Antwort ist, dass die Leser-Monade 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 runReader definiert. Wie steht es also mit den anderen Teilen der API? Nun, jedes Monad ist 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. ask ist ganz einfach:

ask = Reader $ \x -> x

während local nicht so schlimm ist.

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

Okay, die Leser-Monade ist also 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. Außerdem ist ask nur id und local ist nur die Funktionszusammensetzung mit der Reihenfolge der umgeschalteten Funktionen!

150
Philip JF

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

Zum Beispiel schrieb ich irgendwann einen Code, um mit historical Werten umzugehen; Werte, die sich mit der Zeit ändern. Ein sehr einfaches Modell hierfür sind Funktionen vom Zeitpunkt 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 Instanz Applicative bedeutet, dass, wenn Sie employees :: History Day [Person] und customers :: History Day [Person] du kannst das:

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

Das heißt, Functor und Applicative ermöglichen es uns, reguläre, nicht-historische Funktionen anzupassen, um mit Historien zu arbeiten.

Die Monadeninstanz wird am intuitivsten unter Berücksichtigung der Funktion (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c. Eine Funktion vom Typ a -> History t b ist eine Funktion, die ein a einem Verlauf von b Werten zuordnet. Sie könnten zum Beispiel getSupervisor :: Person -> History Day Supervisor, und getVP :: Supervisor -> History Day VP. In der Monadeninstanz für History geht es also darum, Funktionen wie diese zu erstellen. beispielsweise, getSupervisor >=> getVP :: Person -> History Day VP ist die Funktion, die für jedes Person die Historie von VPs abruft, die sie hatten.

Nun, diese History Monade ist tatsächlich genau dieselbe wie Reader. History t a ist wirklich das gleiche wie Reader t a (das ist das gleiche wie t -> a).

Ein weiteres Beispiel: Ich habe vor kurzem in Haskell Prototypen erstellt OLAP . Eine Idee hier ist die eines "Hyperwürfels", der eine Abbildung von Schnittpunkten einer Gruppe von Dimensionen auf Werte ist. 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 erreichen wir, indem wir eine Applicative -Instanz für Hypercube definieren:

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 @[email protected] hypercube to its corresponding point 
    -- in @[email protected]
    ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)

Ich habe gerade den obigen History -Code kopiert und die Namen geändert. Wie Sie sehen können, ist Hypercube auch nur Reader.

Es geht weiter und weiter. Beispiel: Sprachinterpreter beschränken sich auf Reader, wenn Sie dieses Modell anwenden:

  • Ausdruck = ein Reader
  • Freie Variablen = Verwendung von ask
  • Testumgebung = Reader Ausführungsumgebung.
  • Bindungskonstrukte = local

Eine gute Analogie ist, dass ein Reader r a repräsentiert ein a mit "Löchern", die verhindern, dass Sie wissen, um welches a es sich handelt. Sie können ein tatsächliches a nur erhalten, wenn Sie ein r zum Ausfüllen der Löcher angeben. 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 a ist das gleiche wie r -> a, weil eine solche Funktion auch intuitiv ein a ist, dem ein r fehlt.

Die Instanzen Functor, Applicative und Monad von Reader sind daher eine sehr nützliche Verallgemeinerung für Fälle, in denen Sie etwas der Art "an a, dem ein r fehlt ", und ermöglichen es Ihnen, diese" unvollständigen "Objekte so zu behandeln, als wären sie vollständig.

Noch eine andere Art, dasselbe zu sagen: ein Reader r a ist etwas, das r verbraucht und a erzeugt, und die Instanzen Functor, Applicative und Monad sind grundlegende Muster für die Arbeit mit Readers. Functor = mache ein Reader, das die Ausgabe eines anderen Reader modifiziert; Applicative = Verbinde zwei Readers mit demselben Eingang und kombiniere ihre Ausgänge; Monad = Prüfe das Ergebnis eines Reader und benutze es, um ein anderes Reader zu konstruieren. Die Funktionen local und withReader = erstellen ein Reader, das die Eingabe in ein anderes Reader ändert.

52
Luis Casillas

In Java oder C++ können Sie problemlos von überall auf eine Variable zugreifen. Probleme treten auf, wenn Ihr Code mehrere Threads ausführt.

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. Die Nachteile sind: 1) Sie können nicht ALLE Variablen auf diese Weise übergeben - die Liste der Eingabeparameter ist einfach umwerfend. 2) In der Reihenfolge der Funktionsaufrufe: fn1 -> fn2 -> fn3 Benötigt die Funktion fn2 Möglicherweise keine Parameter, die Sie von fn1 An fn3 Übergeben.
  • Sie übergeben den Wert in Umfang einer Monade. Nachteil ist: Man muss genau wissen, was Monadenkonzept ist. Das Weitergeben von Werten ist nur eine von vielen Anwendungen, in denen Sie die Monaden verwenden können. Tatsächlich ist die Monadenkonzeption unglaublich mächtig. Sei nicht sauer, wenn du nicht sofort einen Einblick bekommst. Versuche es einfach weiter und lies verschiedene Tutorials. Das Wissen, das Sie bekommen, wird sich auszahlen.

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

19
Dmitry Bespalov