С точки зрения мирян, монада-писатель - это монада, которая позволяет вам «записывать» элементы в «журнал», пока вы создаете значение. Когда вы закончите, вы получите значение, которое вы произвели и журнал, который содержит все, что вы написали. Другими словами, это монада, побочные эффекты которой - «запись вещей в журнал».
Давайте сделаем это более конкретным на примерах как писателя списков, так и (общих) писателей монад. Я буду использовать Haskell здесь, поскольку это оригинальный контекст, в котором были описаны Монады для функционального программирования .
Список писателей Монада
Я предполагаю, что монада "составитель списков" - это запись, в которую записывается элемент (некоторого типа, который мы назовем w
) в список элементов (конечно, типа [w]
). Также выдает значение типа a
. (См. Примечание внизу, если вы сами получаете ошибки при использовании этого кода.)
newtype ListWriter w a = ListWriter { runListWriter :: ([w], a) }
instance Monad (ListWriter w) where
return a = ListWriter ([], a) -- produce an a, don't log anything
ListWriter (ws, a) >>= k =
let ListWriter (xs, a') = k a -- run the action 'k' on the existing value,
in ListWriter (ws ++ xs, a') -- add anything it logs to the existing log,
-- and produce a new result value
-- Add an item to the log and produce a boring value.
-- When sequenced with >>, this will add the item to existing log.
tell :: w -> ListWriter w ()
tell w = ListWriter ([w], ())
ex1 :: ListWriter String Int
ex1 = do
tell "foo"
tell "bar"
return 0
(Примечание: это эквивалентно ex1 = tell "foo" >> tell "bar" >> return 0
, демонстрирующему использование tell
с >>
для добавления элемента в журнал.)
Если мы вычислим runListWriter ex1
в GHCi, мы увидим, что он записал в журнал «foo» и «bar» и выдал значение результата 0
.
λ> runListWriter ex1
(["foo","bar"],0)
(Общий) Писатель Монада
Теперь давайте посмотрим, как мы превращаем это в общую монаду писателя. Монада писателя работает с любыми вещами, которые можно объединить, а не только со списком. В частности, он работает с любым Monoid
:
class Monoid m where
mempty :: m -- an empty m
mappend :: m -> m -> m -- combine two m's into a single m
Списки являются моноидами с []
и (++)
как mempty
и mappend
соответственно. Примером Monoid, не включенным в список, являются суммы целых чисел:
λ> Sum 1 <> Sum 2 -- (<>) = mappend
Sum {getSum = 3}
Тогда монада писателя
newtype Writer w m = Writer { runWriter :: (w, m) }
Вместо списка w
у нас просто один w. Но когда мы определяем Monad, мы гарантируем, что w
является Monoid
, поэтому мы можем начать с пустого журнала и добавить новую запись в журнал:
instance Monoid w => Monad (Writer w) where
return a = Writer (mempty, a) -- produce an a, don't log anything
Writer (w, a) >>= k =
let Writer (x, a') = k a -- we combine the two w's rather than
in Writer (w <> x, a') -- (++)-ing two lists
Обратите внимание на различия здесь: мы используем mempty
вместо []
и (<>)
вместо (++)
. Так мы обобщаем списки на любой моноид.
Таким образом, писательская монада - это обобщение монады списков на произвольные вещи, которые можно комбинировать, а не просто списки. Вы можете использовать списки с Writer
, чтобы получить что-то (почти) эквивалентное ListWriter
. Единственное отличие состоит в том, что вы должны поместить свой зарегистрированный элемент в список, когда добавляете его в журнал:
ex2 :: Writer [String] Int
ex2 = do
tell ["foo"]
tell ["bar"]
return 0
но вы получите тот же результат:
λ> runWriter ex2
(["foo","bar"],0)
Это потому, что вместо регистрации «элемента, который будет помещен в список», вы регистрируете «список». (Это означает, что вы можете регистрировать несколько элементов одновременно, передавая список из более чем одного элемента.)
В качестве примера использования Writer без списка рассмотрите возможность подсчета сравнений, которые выполняет функция сортировки. Каждый раз, когда ваша функция сравнивается, вы можете tell (Sum 1)
. (Вы можете сказать кому-нибудь. Получите это? Включена ли эта штука?) Затем, в конце, вы получите общий счет (то есть сумму) всех сравнений вместе с отсортированным списком.
ПРИМЕЧАНИЕ. Если вы попытаетесь использовать эти определения ListWriter
и Writer
самостоятельно, GHC сообщит вам, что вы пропускаете экземпляры Functor
и Applicative
. Если у вас есть экземпляр Monad
, вы можете написать остальные в следующих терминах:
import Control.Monad (ap, liftM)
instance Functor (ListWriter w) where
fmap = liftM
instance Applicative (ListWriter w) where
pure = return
(<*>) = ap
И также для Writer
. Я выбрал их выше для ясности.