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)


Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow