Haskell: 在RWS上的monadic fixpoint如果在参数上遍历时会循环。

huangapple go评论71阅读模式
英文:

Haskell: monadic fixpoint on RWS is looping if traversing on argument

问题


我正在编写一个涉及使用`RWS`的程序,用于跟踪可变状态并生成一些日志。我的目的是定义一个计算,评估某些操作,收集随后的状态,并根据它在`Writer`的日志“开头”附加一些内容。最简示例:

```haskell
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并不在很大程度上改变状态。为什么这个程序无法收敛?有什么方法可以修复(在“hehe fix”之前)这个问题吗?

我知道可以使用RWSState部分来解决此问题,但由于某种原因,我想避免这样做。```

英文:

I am writing a program which involves RWS for tracking mutable state and producing some log. My purpose is to define a computation that evaluates some action, gathers the aftercoming state and depending on it appends something to the beginning of the Writer's log. Minimal example:

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

Here I use MonadFix to alter the past by writing to the log before the act has ever happened. And it works flawlessly returning "1ab". However, if I use the M to traverse over the state then it hangs:

prepend s = forM_ s (tell . show)

This behavior is very strange to me, I don't get why does this computation diverge. It is even harder to justify because the prepend in the second variant does not alter the state to any extent. Why doesn't this program converge? Is there anything I can do to fix (inb4 "hehe fix") it?

I know that I can workaround it using State part of RWS, but for some reason I would like to avoid it.

答案1

得分: 3

forM_ s u 只在s已定义的情况下定义,但在这里,s是由mfix传递的占位符,它只在整个计算prepend s >> act >> get终止时才会被定义。

你的第一个版本工作正常,因为它不需要检查状态就能将其放入一对中。

mfix :: (a -> m a) -> m a 不接受严格函数 f :: a -> m a(即 f undefined = undefined 的情况)。

如果你有一组要tell的事物,那么一种更懒惰的方法是在告诉它们之前将它们连接起来:

prepend s = tell (concatMap show s)
英文:

forM_ s u is defined only if s is defined, but here s is a placeholder passed by mfix, which will be defined only once the whole computation prepend s >> act >> get terminates.

Your first version works because it doesn't need to inspect the state to put it in a pair.

mfix :: (a -> m a) -> m a doesn't accept strict functions f :: a -> m a (i.e., such that f undefined = undefined).

If you have a list of things to tell, then a lazier way is to concatenate them before telling them:

prepend s = tell (concatMap show s)

答案2

得分: 3

这是因为 forM_ 不是 惰性的。这个要求在 the mfix documentation 中明确提到:函数必须是惰性的,才能使不动点收敛。但 forM_ 需要解构其参数以便迭代它。它仍然可以对列表的每个元素保持惰性,但不是列表本身。


当你运行这个类似递归的计算时,它需要三个步骤(即三个单子绑定):首先是 prepend,然后是 act,最后是 get,因此你最终得到一个如下所示的值:

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

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

然而,如果你用 foldM_ 替换 prepend,那么你的计算中就不再有三个步骤(三个单子绑定)。现在,对于结果状态列表中的每个元素,都需要一个步骤。这意味着为了构建计算(即定义其步骤,即绑定),甚至需要检查其自己的结果状态。

英文:

This happens because forM_ is not lazy. This requirement is explicitly called out in the mfix documentation: the function has to be lazy in order for the fixpoint to converge. But forM_ does need to destructure its parameter in order to iterate over it. It can still stay lazy with respect to every element of the list, but not the list itself.


When you run this recursive-ish computation of yours, it takes three steps (i.e. three monadic binds): prepend, then act, and then get, and as a result you essentially get a value looking like this:

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

Where the foldMap show s piece is not yet evaluated - i.e. it's a thunk pointing at s, which is the final state of the same computation. It is possible to reference the state to incorporate it into the foldMap show s expression before the state is even evaluated, because Haskell is lazy. This is laziness at work.

However, if you replace prepend with foldM_, you no longer have three steps (three monadic binds) in your computation. Now, you have one step for each element of the resulting state list. Which means that in order to even construct the computation (i.e. define its steps, aka binds) you need to examine its own resulting state.

huangapple
  • 本文由 发表于 2020年1月3日 19:41:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/59578000.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定