首页 > 解决方案 > writer monad 和 list writer monad 有什么区别

问题描述

我正在查看 writer monad 的示例以了解它是如何工作的,几乎所有这些看起来都像 list writer monad。我知道列表作家单子是作家单子的一种。但真正用外行的话来说是作家单子。

标签: functional-programmingmonads

解决方案


通俗地说,writer monad 是让你在产生值的同时将项目“写入”到“日志”的 monad。完成后,您将得到您生成的值包含您编写的所有内容的日志。换句话说,monad 的副作用是“将内容写入日志”。

让我们通过列表编写器和(通用)编写器 monad 的示例使这一点更加具体。我将在这里使用 Haskell,因为它是描述函数式编程的 Monad的原始上下文。

列表编写者 Monad

我假设“列表编写器”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,演示了使用tellwith>>向日志添加项目。)

如果我们runListWriter ex1在 GHCi 中进行评估,我们会看到它在日志中写入了“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,分别带有[](++)asmemptymappend。Monoid 的非列表示例是整数之和:

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

那么作家单子是

newtype Writer w m = Writer { runWriter :: (w, m) }

w我们只有一个 w,而不是 's 列表。但是当我们定义 Monad 时,我们确保它w是 a 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)

这是因为您不是在记录“将被放入列表中的项目”,而是在记录“列表”。(这确实意味着您可以通过传递多个元素的列表来同时记录多个项目。)

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


注意:如果您尝试自己使用这些ListWriter和定义,WriterGHC 会告诉您缺少实例。一旦你有了实例,你可以用它的术语来写其他的:FunctorApplicativeMonad

import Control.Monad (ap, liftM)

instance Functor (ListWriter w) where
  fmap = liftM

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

同样对于Writer. 为了清楚起见,我在上面省略了它们。


推荐阅读