我正在编写一个包含RWS的程序,用于跟踪可变状态并生成一些日志。我的目的是定义一个计算,该计算对某些操作进行评估,收集后继状态,并根据此状态将一些内容附加到Writer日志的开头。最小示例:

type M = RWS () String [Int]

run a = evalRWS a () []

prepend s = tell (foldMap show s)

act = do
  tell "a"
  modify (1:)
  tell "b"

comp = mfix $ \s -> prepend s >> act >> get

在这里,我使用MonadFix通过在act发生之前写日志来更改过去。它可以完美地返回"1ab"。但是,如果我使用M遍历状态,则会挂起:
prepend s = forM_ s (tell . show)

这种行为对我来说很奇怪,我不明白为什么这种计算会有所不同。由于第二个变体中的prepend不会在任何程度上改变状态,因此更难辩解。为什么该程序不收敛?我有什么可以解决的(inb4“hehe fix”)吗?

我知道我可以使用StateRWS部分解决该问题,但是由于某些原因,我想避免它。

最佳答案

发生这种情况是因为forM_不是惰性的。在the mfix documentation中明确调用了此要求:该函数必须是惰性的,以便收敛定点。但是forM_确实需要解构其参数以便对其进行迭代。对于列表中的每个元素,它仍然可以保持懒惰,但对于列表本身而言却可以。

当您运行这种递归式计算时,它需要三个步骤(即三个monadic绑定(bind)):prepend,然后act,然后是get,因此,您实际上得到的值看起来像这样:

[foldMap show s, "a", "b"]

尚未评估foldMap show s片段的地方-即它是一个笨拙的指向s,这是同一计算的最终状态。因为Haskell是惰性的,所以可以甚至在评估状态之前引用该状态以将其合并到foldMap show s表达式中。这是工作的懒惰。

但是,如果将prepend替换为foldM_,则您的计算将不再包含三个步骤(三个Monadic绑定(bind))。现在,对于结果状态列表的每个元素,您都有一个步骤。这意味着要构建计算(即定义其步骤,也称为绑定(bind)),您需要检查其自身的结果状态。

07-27 13:42