Haskell Language
Transformatory Monad
Szukaj…
Licznik monadyczny
Przykład, jak skomponować czytnik, program piszący i monadę stanu za pomocą transformatorów monady. Kod źródłowy można znaleźć w tym repozytorium
Chcemy zaimplementować licznik, który zwiększa jego wartość o daną stałą.
Zaczynamy od zdefiniowania niektórych typów i funkcji:
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)
Załóżmy, że chcemy wykonać następujące obliczenia za pomocą licznika:
- ustaw licznik na 0
- ustaw stałą przyrostu na 3
- zwiększ licznik 3 razy
- ustaw stałą przyrostu na 5
- zwiększ licznik 2 razy
Monada stanowa dostarcza abstrakcje do przekazywania stanu. Możemy wykorzystać monadę stanu i zdefiniować naszą funkcję przyrostową jako transformator stanu.
-- | CounterS is a monad.
type CounterS = State Counter
-- | Increment the counter by 'n' units.
incS :: Int-> CounterS ()
incS n = modify (\c -> inc c n)
To pozwala nam już wyrazić obliczenia w bardziej przejrzysty i zwięzły sposób:
-- | The computation we want to run, with the state monad.
mComputationS :: CounterS ()
mComputationS = do
incS 3
incS 3
incS 3
incS 5
incS 5
Ale wciąż musimy przekazywać stałą przyrostu przy każdym wywołaniu. Chcielibyśmy tego uniknąć.
Dodawanie środowiska
Monada czytnika zapewnia wygodny sposób na przekazanie środowiska. Ta monada jest używana w programowaniu funkcjonalnym do wykonywania tego, co w świecie OO jest znane jako wstrzykiwanie zależności .
W najprostszej wersji monada czytnika wymaga dwóch typów:
rodzaj odczytywanej wartości (tj. nasze środowisko,
r
poniżej),wartość zwrócony przez monadzie Reader ( poniżej).
a
Reader ra
Musimy jednak również skorzystać z monady państwowej. Dlatego musimy użyć transformatora ReaderT
:
newtype ReaderT r m a :: * -> (* -> *) -> * -> *
Za pomocą ReaderT
możemy zdefiniować nasz licznik ze środowiskiem i podać następujący stan:
type CounterRS = ReaderT Int CounterS
Definiujemy funkcję incR
która pobiera stałą przyrostu ze środowiska (za pomocą ask
), i aby zdefiniować naszą funkcję przyrostu w kategoriach naszej monady CounterS
, korzystamy z funkcji lift
(która należy do klasy transformatora monad ).
-- | Increment the counter by the amount of units specified by the environment.
incR :: CounterRS ()
incR = ask >>= lift . incS
Za pomocą monady czytnika możemy zdefiniować nasze obliczenia w następujący sposób:
-- | 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
Wymagania się zmieniły: potrzebujemy logowania!
Załóżmy teraz, że chcemy dodać rejestrowanie do naszych obliczeń, abyśmy mogli zobaczyć ewolucję naszego licznika w czasie.
Mamy też monadę do wykonania tego zadania, monadę pisarza . Podobnie jak w przypadku monady czytającej, ponieważ je komponujemy, musimy skorzystać z transformatora monadowego czytnika:
newtype WriterT w m a :: * -> (* -> *) -> * -> *
Tutaj w
reprezentuje typ wyjścia akumulowanie (która musi być monoid, które pozwalają, aby gromadzić tej wartości), m
jest wewnętrzną monada i typ obliczeń. a
Następnie możemy zdefiniować nasz licznik za pomocą rejestrowania, środowiska i stanu w następujący sposób:
type CounterWRS = WriterT [Int] CounterRS
Korzystając z lift
, możemy zdefiniować wersję funkcji przyrostu, która rejestruje wartość licznika po każdym przyrostu:
incW :: CounterWRS ()
incW = lift incR >> get >>= tell . (:[]) . cValue
Teraz obliczenia zawierające rejestrowanie można zapisać w następujący sposób:
mComputationWRS :: CounterWRS ()
mComputationWRS = do
local (const 3) $ do
incW
incW
incW
local (const 5) $ do
incW
incW
Robienie wszystkiego za jednym zamachem
Ten przykład miał na celu pokazanie transformatorów monad w pracy. Możemy jednak osiągnąć ten sam efekt, łącząc wszystkie aspekty (środowisko, stan i rejestrowanie) w pojedynczej operacji przyrostowej.
W tym celu wykorzystujemy ograniczenia typu:
inc' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
inc' = ask >>= modify . (flip inc) >> get >>= tell . (:[]) . cValue
Tutaj dochodzimy do rozwiązania, które będzie działać dla każdej monady, która spełnia powyższe ograniczenia. Funkcję obliczeniową definiuje się zatem za pomocą typu:
mComputation' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
ponieważ w jego ciele używamy inc ”.
Możemy ghci
to obliczenie, na przykład w ghci
REPL, w następujący sposób:
runState ( runReaderT ( runWriterT mComputation' ) 15 ) (MkCounter 0)