writer monad 和 list writer monad 有什么区别

What is the difference between a writer monad and a list writer monad

我正在查看 writer monad 的示例以了解其工作原理,几乎所有这些看起来都像列表 writer monad。我知道 list writer monad 是 writer monad 的一种。但是,通俗地说,writer monad 到底是什么。

通俗地说,writer monad 是让你 "write" items to a "log" while you produce a value 的 monad。完成后,您最终会得到您生成的值 包含您编写的所有内容的日志。换句话说,就是monad的副作用是"writing things to a log".

让我们通过列表编写器和(通用)编写器 monad 的示例使其更加具体。我将在这里使用 Haskell,因为它是描述 Monads for Functional Programming 的原始上下文。

列表编写器 Monad

我假设 "list writer" monad 是将一个项目(我们称之为 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>> 向日志添加项目。)

如果我们在 GHCi 中评估 runListWriter ex1,我们会看到它将 "foo" 和 "bar" 写入日志并生成结果值 0.

λ> runListWriter ex1
(["foo","bar"],0)

(通用)编写器 Monad

现在,让我们看看如何将它变成通用的 writer monad。 writer monad 可以处理任何可以组合在一起的东西,而不仅仅是一个列表。具体来说,它适用于任何 Monoid:

class Monoid m where
  mempty :: m            -- an empty m
  mappend :: m -> m -> m -- combine two m's into a single m

列表是一个 Monoid,其中 [](++) 分别为 memptymappend。 Monoid 的一个 non-list 例子是整数的总和:

λ> Sum 1 <> Sum 2        -- (<>) = mappend
Sum {getSum = 3}

然后是 writer monad

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 而不是 [](<>) 而不是 (++)。这就是我们如何从列表推广到任何 Monoid。

所以 writer monad 实际上是 list monad 到可以组合的任意事物的泛化,而不仅仅是列表。您可以使用带有 Writer 的列表来获得(几乎)等同于 ListWriter 的内容。唯一的区别是,当您将记录的项目附加到日志时,您必须将其包装在列表中:

ex2 :: Writer [String] Int
ex2 = do
  tell ["foo"]
  tell ["bar"]
  return 0

但你得到相同的结果:

λ>  runWriter ex2
(["foo","bar"],0)

这是因为您没有记录 "an item that will be put in a list",而是记录 "a list"。 (这确实意味着您可以通过传递一个以上元素的列表来同时记录多个项目。)

有关 non-list 使用 Writer 的示例,请考虑对排序函数进行的比较进行计数。每次你的函数进行比较时,你可以tell (Sum 1)。 (你可以告诉别人。明白了吗?这个东西开着吗?)然后,最后,你会得到所有比较的总计数(即总和)以及排序列表。


注意:如果您尝试自己使用这些 ListWriterWriter 定义,GHC 会告诉您缺少 FunctorApplicative 实例。一旦你有了 Monad 实例,你就可以用它的术语写其他的:

import Control.Monad (ap, liftM)

instance Functor (ListWriter w) where
  fmap = liftM

instance Applicative (ListWriter w) where
  pure = return
  (<*>) = ap

Writer 也是如此。为了清楚起见,我在上面省略了它们。