В чем разница между монадой писателя и монадой писателя списка - PullRequest
2 голосов
/ 13 апреля 2019

Я смотрел на примеры писательской монады, чтобы понять, как она работает, и почти все они выглядят как монада писателя списков. Я знаю, что монада писателя - это тип монады писателя. Но что на самом деле является писательской монадой с точки зрения непрофессионалов?

1 Ответ

1 голос
/ 23 мая 2019

С точки зрения мирян, монада-писатель - это монада, которая позволяет вам «записывать» элементы в «журнал», пока вы создаете значение. Когда вы закончите, вы получите значение, которое вы произвели и журнал, который содержит все, что вы написали. Другими словами, это монада, побочные эффекты которой - «запись вещей в журнал».

Давайте сделаем это более конкретным на примерах как писателя списков, так и (общих) писателей монад. Я буду использовать 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. Я выбрал их выше для ясности.

...