this question中myAny函数的代码使用foldr。当谓词满足时,它会停止处理无限列表。
我用foldl重写了它:
myAny :: (a -> Bool) -> [a] -> Bool
myAny p list = foldl step False list
where
step acc item = p item || acc
(请注意,步骤函数的参数已正确反转。)
但是,它不再停止处理无限列表。
我试图跟踪Apocalisp's answer中的函数执行:
myAny even [1..]
foldl step False [1..]
step (foldl step False [2..]) 1
even 1 || (foldl step False [2..])
False || (foldl step False [2..])
foldl step False [2..]
step (foldl step False [3..]) 2
even 2 || (foldl step False [3..])
True || (foldl step False [3..])
True
但是,这不是函数的行为方式。这怎么回事?
答案 0 :(得分:209)
fold
的差异似乎经常引起混淆,所以这里有一个更概括的概述:
考虑使用某个函数[x1, x2, x3, x4 ... xn ]
和种子f
折叠n个值z
的列表。
foldl
是:f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
foldl (flip (:)) []
撤消列表。foldr
是:f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
f
应用于下一个值以及折叠列表其余部分的结果。foldr (:) []
不会更改列表。这里有一个稍微微妙的点,有时会引起人们的注意:由于foldl
向后,f
的每个应用都会添加到外部结果;并且因为它是 lazy ,所以在需要结果之前不会对任何内容进行评估。这意味着要计算结果的任何部分,Haskell首先遍历整个列表构建嵌套函数应用程序的表达式,然后计算最外层函数,将其参数计算为需要。如果f
总是使用它的第一个参数,这意味着Haskell必须一直递归到最里面的术语,然后向后计算f
的每个应用程序。
这显然与大多数功能程序员所熟悉和喜爱的有效尾递归相差甚远!
实际上,即使foldl
在技术上是尾递归的,因为整个结果表达式是在评估任何内容之前构建的, foldl
会导致堆栈溢出!
另一方面,请考虑foldr
。它也很懒,但因为它运行前进,f
的每个应用程序都会添加到结果的内部中。因此,为了计算结果,Haskell构造了一个单个函数应用程序,其第二个参数是折叠列表的其余部分。如果f
在其第二个参数(例如数据构造函数)中是惰性的,则结果将是递增延迟,折叠的每个步骤仅在结果的某些部分计算时需要它的是评估。
因此,当foldr
没有时,我们可以看到为什么foldl
有时会在无限列表上工作:前者可以懒惰地将无限列表转换为另一个惰性无限数据结构,而后者必须检查整个列表生成结果的任何部分。另一方面,foldr
具有立即需要两个参数的函数,例如(+)
,与foldl
非常相似(或者更确切地说,不起作用),之前构建一个巨大的表达式评估它。
因此需要注意的两点是:
foldr
可以将一个懒惰的递归数据结构转换为另一个。 您可能已经注意到,foldr
可以完成foldl
可以做的所有事情,还有更多。这是真的!事实上, foldl几乎没用!
但是如果我们想通过折叠大(但不是无限)列表来产生非惰性结果呢?为此,我们需要严格弃牌,the standard libraries thoughfully provide:
foldl'
是:f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
foldl' (flip (:)) []
撤消列表。因为foldl'
是严格,要计算结果,Haskell将在每一步评估 f
,而不是让左参数累积巨大的,无价值的表达。这给了我们想要的通常,有效的尾递归!换句话说:
foldl'
可以有效地折叠大型列表。 foldl'
将在无限列表中挂起无限循环(不会导致堆栈溢出)。 Haskell维基也有a page discussing this。
答案 1 :(得分:26)
myAny even [1..]
foldl step False [1..]
foldl step (step False 1) [2..]
foldl step (step (step False 1) 2) [3..]
foldl step (step (step (step False 1) 2) 3) [4..]
等
直观地,foldl
始终位于“外部”或“左侧”,因此首先展开。无限无限。
答案 2 :(得分:10)
你可以在Haskell的文档here中看到,foldl是尾递归的,如果传递一个无限列表将永远不会结束,因为它在返回一个值之前调用自己的下一个参数...
答案 3 :(得分:0)
我不知道Haskell,但在Scheme中,fold-right
将始终在列表的最后一个元素上“行动”。因此对循环列表(与无限循环列表相同)不起作用。
我不确定fold-right
是否可以写尾递归,但对于任何循环列表,你应该得到堆栈溢出。 fold-left
OTOH通常使用尾递归实现,如果不及早终止,它将陷入无限循环。