Haskell Language
Monad Transformatoren
Suche…
Ein monadischer Zähler
Ein Beispiel, wie Leser, Schreiber und Zustandsmonade mithilfe von Monadetransformatoren erstellt werden. Der Quellcode befindet sich in diesem Repository
Wir möchten einen Zähler implementieren, der seinen Wert um eine bestimmte Konstante erhöht.
Wir beginnen mit der Definition einiger Typen und Funktionen:
newtype Counter = MkCounter {cValue :: Int}
deriving (Show)
-- | 'inc c n' increments the counter by 'n' units.
inc :: Counter -> Int -> Counter
inc (MkCounter c) n = MkCounter (c + n)
Angenommen, wir möchten die folgende Berechnung mit dem Zähler ausführen:
- Setzen Sie den Zähler auf 0
- Setze die Inkrementkonstante auf 3
- Erhöhen Sie den Zähler dreimal
- Setze die Inkrementkonstante auf 5
- Erhöhen Sie den Zähler zweimal
Die Zustandsmonade stellt Abstraktionen für die Weitergabe von Zuständen bereit. Wir können die Zustandsmonade nutzen und unsere Zuwachsfunktion als Zustandstransformator definieren.
-- | CounterS is a monad.
type CounterS = State Counter
-- | Increment the counter by 'n' units.
incS :: Int-> CounterS ()
incS n = modify (\c -> inc c n)
Dies ermöglicht es uns bereits, eine Berechnung klarer und prägnanter auszudrücken:
-- | The computation we want to run, with the state monad.
mComputationS :: CounterS ()
mComputationS = do
incS 3
incS 3
incS 3
incS 5
incS 5
Die Inkrementkonstante muss jedoch bei jedem Aufruf weitergegeben werden. Das möchten wir gerne vermeiden.
Umgebung hinzufügen
Die Leser-Monade bietet eine bequeme Möglichkeit, eine Umgebung zu umgehen. Diese Monade wird in der Funktionsprogrammierung verwendet, um in der OO-Welt sogenannte Abhängigkeitsinjektion durchzuführen.
In der einfachsten Version erfordert die Leser-Monade zwei Arten:
der Typ des Wertes gelesen werden (dh unsere Umwelt,
r
unten),der von der Lesermonade zurückgegebene Wert (
a
unten).Leser ra
Allerdings müssen wir auch die Staatsmonade nutzen. Daher müssen wir den ReaderT
Transformator verwenden:
newtype ReaderT r m a :: * -> (* -> *) -> * -> *
Mit ReaderT
können wir unseren Zähler mit Umgebung und Status wie folgt definieren:
type CounterRS = ReaderT Int CounterS
Wir definieren eine incR
Funktion, die die Inkrement-Konstante aus der Umgebung (mithilfe von ask
) entnimmt, und um unsere Inkrement-Funktion in Bezug auf unsere CounterS
Monade zu definieren, verwenden wir die lift
Funktion (die zur Monad-Transformator- Klasse gehört).
-- | Increment the counter by the amount of units specified by the environment.
incR :: CounterRS ()
incR = ask >>= lift . incS
Mit der Lesermonade können wir unsere Berechnung folgendermaßen definieren:
-- | The computation we want to run, using reader and state monads.
mComputationRS :: CounterRS ()
mComputationRS = do
local (const 3) $ do
incR
incR
incR
local (const 5) $ do
incR
incR
Die Anforderungen haben sich geändert: Wir brauchen Protokollierung!
Nehmen wir nun an, dass wir unsere Berechnungen um Protokollierung erweitern möchten, damit wir die Entwicklung unseres Zählers rechtzeitig sehen können.
Wir haben auch eine Monade, um diese Aufgabe auszuführen, die Schriftsteller-Monade . Wie bei der Reader-Monade müssen wir, da wir sie komponieren, den Reader-Monad-Transformator verwenden:
newtype WriterT w m a :: * -> (* -> *) -> * -> *
Hier w
die Art der Ausgabe zu akkumulieren darstellt (die eine monoid sein muss, die es uns ermöglichen , diesen Wert zu akkumulieren), m
ist der innere monadisch, und a
die Art der Berechnung.
Wir können dann unseren Zähler mit Protokollierung, Umgebung und Status wie folgt definieren:
type CounterWRS = WriterT [Int] CounterRS
Mithilfe von lift
können Sie die Version der Inkrementierungsfunktion definieren, die den Wert des Zählers nach jedem Inkrement protokolliert:
incW :: CounterWRS ()
incW = lift incR >> get >>= tell . (:[]) . cValue
Nun kann die Berechnung, die die Protokollierung enthält, wie folgt geschrieben werden:
mComputationWRS :: CounterWRS ()
mComputationWRS = do
local (const 3) $ do
incW
incW
incW
local (const 5) $ do
incW
incW
Alles auf einmal machen
Dieses Beispiel soll Monadentransformatoren bei der Arbeit zeigen. Der gleiche Effekt kann jedoch erzielt werden, wenn alle Aspekte (Umgebung, Status und Protokollierung) in einem einzigen Inkrementierungsvorgang zusammengestellt werden.
Dazu verwenden wir Typeinschränkungen:
inc' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
inc' = ask >>= modify . (flip inc) >> get >>= tell . (:[]) . cValue
Hier kommen wir zu einer Lösung, die für jede Monade funktioniert, die die oben genannten Einschränkungen erfüllt. Die Berechnungsfunktion ist also mit type definiert:
mComputation' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
da wir in ihrem Körper inc verwenden.
Wir können diese Berechnung beispielsweise in der ghci
REPL wie folgt ghci
:
runState ( runReaderT ( runWriterT mComputation' ) 15 ) (MkCounter 0)