Haskell Language
Monad Transformers
Sök…
En monadisk räknare
Ett exempel på hur man komponerar läsaren, författaren och statlig monad med monadtransformatorer. Källkoden finns i detta arkiv
Vi vill implementera en räknare som ökar dess värde med en given konstant.
Vi börjar med att definiera vissa typer och funktioner:
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)
Antag att vi vill utföra följande beräkning med räknaren:
- ställ in räknaren till 0
- ställ in steget konstant till 3
- öka räknaren 3 gånger
- ställ steget konstant på 5
- öka räknaren 2 gånger
Den statliga monaden tillhandahåller abstraktioner för att passera tillstånd. Vi kan använda statens monad och definiera vår tilläggsfunktion som en statstransformator.
-- | CounterS is a monad.
type CounterS = State Counter
-- | Increment the counter by 'n' units.
incS :: Int-> CounterS ()
incS n = modify (\c -> inc c n)
Detta ger oss redan möjlighet att uttrycka en beräkning på ett mer tydligt och kortfattat sätt:
-- | The computation we want to run, with the state monad.
mComputationS :: CounterS ()
mComputationS = do
incS 3
incS 3
incS 3
incS 5
incS 5
Men vi måste fortfarande passera inkrementskonstanten vid varje åkallelse. Vi skulle vilja undvika detta.
Lägga till en miljö
Läsarmonaden ger ett bekvämt sätt att passera en miljö runt. Denna monad används i funktionell programmering för att utföra vad i OO-världen kallas beroendeinjektion .
I sin enklaste version kräver läsarmonaden två typer:
vilken typ av värde som läses (dvs. vår miljö,
r
nedan),värdet som returneras av läsarmonaden (
a
nedan).Läsare ra
Men vi måste också använda statsmonaden. Därför måste vi använda ReaderT
transformatorn:
newtype ReaderT r m a :: * -> (* -> *) -> * -> *
Med ReaderT
kan vi definiera vår räknare med miljö och tillstånd enligt följande:
type CounterRS = ReaderT Int CounterS
Vi definierar en incR
funktion som tar steget konstant från miljön (med ask
), och för att definiera vår inkrementsfunktion i termer av vår CounterS
monad använder vi lift
(som tillhör monad-transformatorklassen ).
-- | Increment the counter by the amount of units specified by the environment.
incR :: CounterRS ()
incR = ask >>= lift . incS
Med hjälp av läsarmonaden kan vi definiera vår beräkning på följande sätt:
-- | 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
Kraven ändrades: vi måste logga in!
Antag nu att vi vill lägga till loggning i vår beräkning, så att vi kan se utvecklingen av vår räknare i tid.
Vi har också en monad för att utföra denna uppgift, författarens monad . Liksom med läsarmonaden, eftersom vi komponerar dem, måste vi använda läsaren monad-transformator:
newtype WriterT w m a :: * -> (* -> *) -> * -> *
Här representerar w
typen av utgången som ska ackumuleras (som måste vara en monoid, som tillåter oss att ackumulera detta värde), m
är den inre monaden och a
typ av beräkningen.
Vi kan sedan definiera vår räknare med loggning, miljö och tillstånd enligt följande:
type CounterWRS = WriterT [Int] CounterRS
Och med hjälp av lift
vi definiera versionen av inkrementfunktionen som loggar räknarens värde efter varje steg:
incW :: CounterWRS ()
incW = lift incR >> get >>= tell . (:[]) . cValue
Nu kan beräkningen som innehåller loggning skrivas enligt följande:
mComputationWRS :: CounterWRS ()
mComputationWRS = do
local (const 3) $ do
incW
incW
incW
local (const 5) $ do
incW
incW
Att göra allt på en gång
Detta exempel är avsett att visa monadtransformatorer på jobbet. Men vi kan uppnå samma effekt genom att komponera alla aspekter (miljö, tillstånd och loggning) i en enda tillväxtoperation.
För att göra detta använder vi typ-begränsningar:
inc' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
inc' = ask >>= modify . (flip inc) >> get >>= tell . (:[]) . cValue
Här kommer vi till en lösning som fungerar för alla monader som uppfyller ovanstående begränsningar. Beräkningsfunktionen definieras således med typen:
mComputation' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
eftersom vi i sin kropp använder inc.
Vi kan köra denna beräkning, till exempel i ghci
REPL, på följande sätt:
runState ( runReaderT ( runWriterT mComputation' ) 15 ) (MkCounter 0)