在尝试使用Haskell定义从连接通道读取的无限循环时,我遇到了使用Kleisli monad组合折叠无限的部分应用函数列表的想法。这个想法看起来似乎有道理和简单,但我遇到了一个奇怪的场景,显然正在评估无限列表,而不是等待输入。
我创建了以下示例来说明我的问题:
loop :: Int -> IO Int
loop s = (foldr (<=<) return (repeat process)) s
process :: Int -> IO Int
process i = do
putStrLn $ show i
x <- getLine
return $ i + 1
main = loop 0
当我运行上面的代码时,GHCi在堆栈溢出后停止。它不等待“getLine”。当我替换&lt; =&lt;使用&gt; =&gt;,它按预期工作:列表只会在每个“getLine”之后按元素获取元素。
为什么会这样?我的foldr初始值是return,它不应该对它添加任何副作用,并且根据monad定律,左右同一性应该保证使用&lt; =&lt;和&gt; =&gt;。
答案 0 :(得分:3)
你无法从它的结尾开始访问无限列表 - 没有尽头。
您编码构建
action0 <=< (action1 <=< (action2 ....))
具有正确的关联性,但 x <=< y
(粗略地说)在y
之前运行x
。
使用>=>
可使计算从action0
开始。
与此比较:
cons x xs = x : xs
snoc x xs = xs ++ [x]
foldr cons [] [1..] = [1..]
foldr snoc [] [1..] = _|_
这并不奇怪,因为foldr snoc [] = reverse
,我们无法反转无限列表。
这里的问题是snoc
在xs
之前评估尾部x
,因此foldr
评估&#34;递归调用&#34;在考虑手头的元素之前到尾部列表。
在monadic案件中,事情更复杂,但总体原则的精神相似。使用foldr (<=<) return (repeat action)
类似于
loop x = do
x' <- loop x
action x'
相反,foldr (>=>) return (repeat action)
类似于
loop x = do
x' <- action x
loop x'
答案 1 :(得分:1)
尝试展开foldr
:
foldr (<=<) return (repeat process) s
-- becomes
(process <=< (process <=< (process … <=< return))) s
必须评估最内层函数以应用s
- 当然失败。相比之下,
foldr (>=>) return (repeat process) s
-- becomes
(process >=> (process >=> (process … >=> return))) s
当应用s
时会将它传递给链中的第一个函数,并且只有在将结果传递给下一个函数时,才会评估链的其余部分 - 懒惰。
我认为没有办法构建序列
(return >=> (process >=> (process >=> (process …)))) s
使用折叠,但您可以使用fixpoint combinator:
fix (process >=>) s -- `return >=>` is a pointless `id` in front
-- or
fix (<=< process) s
请注意,非常相似的fix (>=> process)
和fix (process <=<)
将具有相同的无限递归问题,尝试永远找到要应用的第一个函数。
答案 2 :(得分:1)
foldr
的工作原理如下:
xs =
a : (b : (c : ... (d : nil)...))
foldr kons knil xs =
a `kons` (b `kons` (c `kons` ... (d `kons` knil)...))
所以,在你的情况下:
repeat process =
process : (process : (process : ...))
foldr (<=<) return (repeat process) =
process <=< (process <=< (process <=< ...))
现在,当您执行应用(f <=< g) x
时,您必须先应用g x
。在您的情况下,要申请(process <=< (process <=< (process <=< ...))) x
,您必须先申请(process <=< (process <=< (process <=< ...))) x
,因此您必须先申请(process <=< (process <=< (process <=< ...))) x
,....
解决方案(如您所知)是将(<=<)
翻转为(>=>)
foldr (>=>) return (repeat process) =
process >=> (process >=> (process >=> ...))
要应用(f >=> g) x
,您必须先申请f x
。在您的情况下,要申请(process >=> (process >=> (process >=> ...))) x
,您必须首先应用process x
,这确实有效,然后您需要应用(process >=> (process >=> (process >=> ...))) x'
,因此您应用process x'
,这确实有效,然后你需要申请(process >=> (process >=> (process >=> ...))) x''
,所以你需要申请process x''
,......。
运行时并不关心结果应该是相同的。检查该属性意味着解决暂停问题,这对于每种可能的情况都是不可能解决的。 Haskell可以预见的失败,而不是试图做不可能的事情,变得更加困惑一千倍。