英文:
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”之前)这个问题吗?
我知道可以使用RWS
的State
部分来解决此问题,但由于某种原因,我想避免这样做。```
英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论