Ricerca…


Un contatore monadico

Un esempio su come comporre il lettore, lo scrittore e la monade di stato usando i trasformatori monad. Il codice sorgente può essere trovato in questo repository

Vogliamo implementare un contatore, che incrementa il suo valore di una determinata costante.

Iniziamo definendo alcuni tipi e funzioni:

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)

Supponiamo di voler eseguire il seguente calcolo utilizzando il contatore:

  • imposta il contatore su 0
  • imposta la costante di incremento su 3
  • incrementare il contatore 3 volte
  • imposta la costante di incremento su 5
  • incrementare il contatore 2 volte

La monade di stato fornisce astrazioni per passare lo stato intorno. Possiamo usare la monade di stato e definire la nostra funzione di incremento come un trasformatore di stato.

-- | CounterS is a monad.
type CounterS = State Counter

-- | Increment the counter by 'n' units.
incS :: Int-> CounterS ()
incS n = modify (\c -> inc c n)

Questo ci consente già di esprimere un calcolo in un modo più chiaro e sintetico:

-- | The computation we want to run, with the state monad.
mComputationS :: CounterS ()
mComputationS = do
  incS 3
  incS 3
  incS 3
  incS 5
  incS 5

Ma dobbiamo ancora passare la costante di incremento ad ogni invocazione. Vorremmo evitare questo.

Aggiungere un ambiente

Il lettore monad fornisce un modo conveniente per passare un ambiente intorno. Questa monade viene utilizzata nella programmazione funzionale per eseguire ciò che nel mondo OO è noto come iniezione di dipendenza .

Nella sua versione più semplice, il lettore monad richiede due tipi:

  • il tipo di valore letto (cioè il nostro ambiente, r sotto),

  • il valore restituito dal lettore monad ( a sotto).

    Reader ra

Tuttavia, dobbiamo anche sfruttare la monade di stato. Pertanto, dobbiamo utilizzare il trasformatore ReaderT :

newtype ReaderT r m a :: * -> (* -> *) -> * -> *

Usando ReaderT , possiamo definire il nostro contatore con ambiente e stato come segue:

type CounterRS = ReaderT Int CounterS

Definiamo una funzione incR che prende la costante di incremento dall'ambiente (usando ask ) e per definire la nostra funzione di incremento in termini di monad CounterS , usiamo la funzione lift (che appartiene alla classe monad transformer ).

-- | Increment the counter by the amount of units specified by the environment.
incR :: CounterRS ()
incR = ask >>= lift . incS

Usando il lettore monad possiamo definire il nostro calcolo come segue:

-- | 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

I requisiti sono cambiati: abbiamo bisogno di registrazione!

Supponiamo ora di voler aggiungere la registrazione al nostro calcolo, in modo da poter vedere l'evoluzione del nostro contatore nel tempo.

Abbiamo anche una monade per svolgere questo compito, la monade scrittore . Come con il lettore monad, dal momento che li stiamo componendo, abbiamo bisogno di utilizzare il trasformatore del lettore Monad:

newtype WriterT w m a :: * -> (* -> *) -> * -> *

Qui w rappresenta il tipo di output da accumulare (che deve essere un monoide, che ci consente di accumulare questo valore), m è la monade interna e a tipo di calcolo.

Possiamo quindi definire il nostro contatore con la registrazione, l'ambiente e lo stato come segue:

type CounterWRS = WriterT [Int] CounterRS

E facendo uso di lift possiamo definire la versione della funzione di incremento che registra il valore del contatore dopo ogni incremento:

incW :: CounterWRS ()
incW = lift incR >> get >>= tell . (:[]) . cValue

Ora il calcolo che contiene la registrazione può essere scritto come segue:

mComputationWRS :: CounterWRS ()
mComputationWRS = do
  local (const 3) $ do
    incW
    incW
    incW
    local (const 5) $ do
      incW
      incW

Fare tutto in una volta

Questo esempio intendeva mostrare i trasformatori monad al lavoro. Tuttavia, possiamo ottenere lo stesso effetto componendo tutti gli aspetti (ambiente, stato e registrazione) in un'unica operazione di incremento.

Per fare questo facciamo uso di vincoli di tipo:

inc' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
inc' = ask >>= modify . (flip inc) >> get >>= tell . (:[]) . cValue

Qui arriviamo ad una soluzione che funzionerà per qualsiasi monade che soddisfi i vincoli di cui sopra. La funzione di calcolo è definita quindi con tipo:

mComputation' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()

dal momento che nel suo corpo facciamo uso di inc '.

Potremmo eseguire questo calcolo, ad esempio in ghci REPL, come segue:

runState ( runReaderT ( runWriterT mComputation' ) 15 )  (MkCounter 0)


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow