Haskell Language
Monad-transformatoren
Zoeken…
Een monadisch loket
Een voorbeeld van het samenstellen van de lezer, schrijver en staatsmonade met behulp van monadetransformatoren. De broncode is te vinden in deze repository
We willen een teller implementeren, die de waarde verhoogt met een gegeven constante.
We beginnen met het definiëren van enkele typen en functies:
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)
Stel dat we de volgende berekening willen uitvoeren met behulp van de teller:
- zet de teller op 0
- stel de incrementconstante in op 3
- verhoog de teller 3 keer
- stel de incrementconstante in op 5
- verhoog de teller 2 keer
De staatsmonade biedt abstracties voor het doorgeven van de staat. We kunnen gebruik maken van de statusmonade en onze incrementfunctie definiëren als een staatstransformator.
-- | CounterS is a monad.
type CounterS = State Counter
-- | Increment the counter by 'n' units.
incS :: Int-> CounterS ()
incS n = modify (\c -> inc c n)
Dit stelt ons al in staat om een berekening op een meer duidelijke en beknopte manier uit te drukken:
-- | The computation we want to run, with the state monad.
mComputationS :: CounterS ()
mComputationS = do
incS 3
incS 3
incS 3
incS 5
incS 5
Maar we moeten nog steeds de incrementconstante passeren bij elke aanroep. We willen dit graag vermijden.
Een omgeving toevoegen
De lezer-monade biedt een handige manier om een omgeving rond te leiden. Deze monade wordt gebruikt in functioneel programmeren om uit te voeren wat in de OO-wereld bekend staat als afhankelijkheidsinjectie .
In zijn eenvoudigste versie heeft de lezer-monade twee typen nodig:
het type van de waarde zijn gelezen (dwz milieu,
r
hieronder)de waarde die wordt geretourneerd door de lezer-monade (
a
hieronder).Reader ra
We moeten echter ook gebruik maken van de staatsmonade. Daarom moeten we de ReaderT
transformator gebruiken:
newtype ReaderT r m a :: * -> (* -> *) -> * -> *
Met ReaderT
kunnen we onze teller met omgeving en status als volgt definiëren:
type CounterRS = ReaderT Int CounterS
We definiëren een incR
functie die de incrementconstante uit de omgeving haalt (met behulp van ask
) en om onze incrementfunctie te definiëren in termen van onze CounterS
monade, maken we gebruik van de lift
(die behoort tot de klasse van de monadetransformator ).
-- | Increment the counter by the amount of units specified by the environment.
incR :: CounterRS ()
incR = ask >>= lift . incS
Met behulp van de lezer-monade kunnen we onze berekening als volgt definiëren:
-- | 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
De vereisten zijn gewijzigd: we hebben logboekregistratie nodig!
Ga er nu van uit dat we logging aan onze berekening willen toevoegen, zodat we de evolutie van onze teller op tijd kunnen zien.
We hebben ook een monade om deze taak uit te voeren, de schrijversmonade . Net als bij de lezer-monade moeten we, omdat we ze componeren, gebruik maken van de lezer-monade-transformator:
newtype WriterT w m a :: * -> (* -> *) -> * -> *
Hier stelt w
het type van de te accumuleren output voor (wat een monoïde moet zijn, waarmee we deze waarde kunnen accumuleren), m
is de binnenste monade en a
het type van de berekening.
We kunnen dan onze teller met logging, omgeving en status als volgt definiëren:
type CounterWRS = WriterT [Int] CounterRS
En met behulp van lift
kunnen we de versie van de incrementfunctie definiëren die de waarde van de teller na elke increment registreert:
incW :: CounterWRS ()
incW = lift incR >> get >>= tell . (:[]) . cValue
Nu kan de berekening die logboekregistratie bevat als volgt worden geschreven:
mComputationWRS :: CounterWRS ()
mComputationWRS = do
local (const 3) $ do
incW
incW
incW
local (const 5) $ do
incW
incW
Alles in één keer doen
Dit voorbeeld was bedoeld om monadetransformatoren aan het werk te tonen. We kunnen echter hetzelfde effect bereiken door alle aspecten (omgeving, status en logboekregistratie) in één incrementele bewerking samen te stellen.
Om dit te doen maken we gebruik van typebeperkingen:
inc' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
inc' = ask >>= modify . (flip inc) >> get >>= tell . (:[]) . cValue
Hier komen we tot een oplossing die werkt voor elke monade die voldoet aan de bovenstaande beperkingen. De berekeningsfunctie wordt dus gedefinieerd met type:
mComputation' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
omdat we in zijn lichaam gebruik maken van inc '.
We zouden deze berekening, bijvoorbeeld in de ghci
REPL, als volgt kunnen uitvoeren:
runState ( runReaderT ( runWriterT mComputation' ) 15 ) (MkCounter 0)