Haskell Language
Monad Transformers
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)