Haskell Language
Monad Transformers
Поиск…
Монадический счетчик
Пример того, как составить читатель, писатель и государственную монаду, используя монадные трансформаторы. Исходный код можно найти в этом репозитории
Мы хотим реализовать счетчик, который увеличивает его значение на заданную константу.
Начнем с определения некоторых типов и функций:
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)