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
- Was bedeutet die Monade tun ? Mit welchen Operationen ist es ausgestattet? Wozu ist es gut?
- 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 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 wirklich einfach:
ask = Reader $ \x -> x
während local
ist 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, ask
ist nur id
und local
ist nur Funktionszusammensetzung mit der Reihenfolge der Funktionen umgeschaltet!