Поиск…


Монадический счетчик

Пример того, как составить читатель, писатель и государственную монаду, используя монадные трансформаторы. Исходный код можно найти в этом репозитории

Мы хотим реализовать счетчик, который увеличивает его значение на заданную константу.

Начнем с определения некоторых типов и функций:

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)

Предположим, что мы хотим выполнить следующее вычисление с использованием счетчика:

  • установите счетчик на 0
  • установить постоянную приращения 3
  • увеличивайте счетчик 3 раза
  • установить постоянную инкремента равной 5
  • увеличивать счетчик в 2 раза

Государственная монада обеспечивает абстракции для прохождения состояния вокруг. Мы можем использовать государственную монаду и определить нашу функцию приращения как трансформатор состояния.

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

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

Это уже позволяет нам более четко и лаконично вычислять вычисления:

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

Но мы все равно должны передавать константу приращения при каждом вызове. Мы бы хотели этого избежать.

Добавление среды

Монада-читатель обеспечивает удобный способ передачи окружения. Эта монада используется в функциональном программировании для выполнения того, что в мире ОО известно как инъекция зависимости .

В своей простейшей версии монада-читатель требует двух типов:

  • тип считываемого значения (т.е. наша среда, r ниже),

  • значение, возвращаемое монада-читателя ( a ниже).

    Читатель ra

Однако нам нужно также использовать государственную монаду. Таким образом, нам необходимо использовать трансформатор ReaderT :

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

Используя ReaderT , мы можем определить наш счетчик со средой и состоянием следующим образом:

type CounterRS = ReaderT Int CounterS

Мы определяем функцию incR которая принимает константу инкремента из среды (используя ask ), и для определения нашей инкрементной функции в терминах нашей монады CounterS мы используем функцию lift (которая принадлежит классу трансформатора монады ).

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

Используя монаду-читателю, мы можем определить наши вычисления следующим образом:

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

Требования изменились: нам нужна регистрация!

Теперь предположим, что мы хотим добавить журнал в наши вычисления, чтобы мы могли видеть эволюцию нашего счетчика во времени.

У нас также есть монада для выполнения этой задачи, монада-писателя . Как и в случае с монадой-читателем, поскольку мы их составляем, нам необходимо использовать монадный трансформатор считывателя:

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

Здесь w представляет тип выхода аккумулировать (который должен быть моноид, что позволило нам накопить это значение), m является внутренней монадой и типа вычислений. a

Затем мы можем определить наш счетчик с протоколированием, средой и состоянием следующим образом:

type CounterWRS = WriterT [Int] CounterRS

И используя lift мы можем определить версию функции приращения, которая регистрирует значение счетчика после каждого приращения:

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

Теперь вычисление, содержащее запись, может быть записано следующим образом:

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

Делать все за один раз

Этот пример предназначен для демонстрации трансформаторов монады. Тем не менее, мы можем добиться такого же эффекта, составив все аспекты (среду, состояние и протоколирование) за одну операцию приращения.

Для этого мы используем ограничения типа:

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

Здесь мы приходим к решению, которое будет работать для любой монады, которая удовлетворяет вышеприведенным ограничениям. Вычислительная функция определяется таким образом:

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

так как в его теле мы используем inc '.

Мы могли бы выполнить это вычисление, например, в ghci например:

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


Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow