Kleisli组成并折叠无限列表

时间:2017-10-31 18:30:10

标签: haskell functional-programming

在尝试使用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;。

3 个答案:

答案 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,我们无法反转无限列表。

这里的问题是snocxs之前评估尾部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可以预见的失败,而不是试图做不可能的事情,变得更加困惑一千倍。