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)


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow