在无限列表上左右折叠


72

我对“了解您的Haskell”(很棒的书imo,不要轻描淡写)中的以下文章有疑问:

一个很大的不同是,右折叠在无限列表上起作用,而左折叠则不行!简而言之,如果您在某个时间点取一个无限的列表,然后将其从右侧折叠起来,最终您将到达列表的开头。但是,如果您在某个点取一个无限的列表,然后尝试将其从左向上折叠,那么您将永远无法结束!

我就是不明白这一点。如果您获取一个无限列表,然后尝试从右侧将其折叠起来,那么您将不得不从无穷大的那一点开始,这只是没有发生(如果有人知道您可以使用的语言,请告诉:p )。至少,您必须根据Haskell的实现从那里开始,因为在Haskell foldr和foldl中,不需要使用确定在列表中应该从何处开始折叠的参数。

我同意报价单iff foldr和foldl的参数确定了它们应该在列表中的何处开始折叠,因为有意义的是,如果您从无限的列表开始并从已定义的索引开始折叠,它将最终终止,而实际上不管你从左折开始 您将向无限折叠。但是,foldr和foldl接受此参数,因此引号没有意义。在Haskell中,无限列表上的左折和右折都不会终止

我的理解正确吗?还是我缺少什么?


3
也许你应该考虑在列相关看看这个问题:stackoverflow.com/questions/833186/...
gatoatigrado

1
foldl并且foldr 从左到右处理列表。可以帮助您更深刻地理解折痕。
Matthias Braun

Answers:


89

这里的关键是懒惰。如果用于折叠列表的函数很严格,则给定无限列表,向左折叠或向右折叠都不会终止。

Prelude> foldr (+) 0 [1..]
^CInterrupted.

但是,如果尝试折叠不太严格的功能,则会得到终止结果。

Prelude> foldr (\x y -> x) 0 [1..]
1

您甚至可以得到一个无限数据结构的结果,因此尽管它在某种意义上不会终止,但仍然可以产生可以延迟使用的结果。

Prelude> take 10 $ foldr (:) [] [1..]
[1,2,3,4,5,6,7,8,9,10]

但是,这不适用于foldl,因为您将永远无法评估最外部的函数调用,无论是否延迟。

Prelude> foldl (flip (:)) [] [1..]
^CInterrupted.
Prelude> foldl (\x y -> y) 0 [1..]
^CInterrupted.

请注意,左右折页之间的关键区别不是列表的遍历顺序(总是从左到右),而是嵌套的结果函数应用程序的顺序。

  • 使用foldr,它们嵌套在“内部”

    foldr f y (x:xs) = f x (foldr f y xs)
    

    在这里,第一次迭代将导致的最外层应用f。因此,f有机会变得懒惰,以便不总是对第二个参数进行求值,或者第二个参数可以在不强制第二个参数的情况下产生数据结构的某些部分。

  • 使用foldl,它们嵌套在“外部”

    foldl f y (x:xs) = foldl f (f y x) xs
    

    在这里,我们无法评估任何事物,直到我们达到的最外层应用为止f,而对于无限列表而言,无论f严格与否,我们都无法达到。


1
这很有趣:foldr(\ xy-> x)0 [1 ..]。这真的是懒惰评估的例子吗?还是仅仅是编译器很聪明?就像,我看不到传统意义上的评估方法,但是很明显最终值是多少。那么,GHC到底是在懒惰地进行评估(如果是,则如何进行***?:P),还是足够聪明地认识到答案始终为1?
TheIronKnuckle

2
@ThelronKnuckle这是懒惰的评估。更确切地说,这是Haskell的非严格语义。无论编译器多么聪明,都不允许对其进行更改。
2011

@ThelronKnuckle除非编译器执行超编译,否则这是惰性计算。
2011年

好的,可能是懒惰的评估,但是在某些时候必须对其进行评估,那么到底发生了什么呢?它会在累加器中粘贴“无穷大”,然后向右折叠并在累加器中变色“无穷大-1”,然后是“无穷大-2”,依此类推,一直下降到1?还是GHC足够聪明,可以看到结果将为1,而是将其代入?
TheIronKnuckle

3
@TheIronKnuckle:评估方式如下:foldr (\x y -> x) 0 [1..] ={definition of foldr} (\x y -> x) 1 (foldr (\x y -> x) 0 [2..]) ={fill in x argument} (\y -> 1) (foldr (\x y -> x) 0 [2..]) ={fill in y argument} 1
Daniel Wagner

17

关键词是“在某个时候”。

如果您在某个时间点取了一个无限列表然后从右侧向上折叠,最终将到达列表的开头。

因此,您是对的,您不可能从无限列表的“最后一个”元素开始。但是作者的观点是:假设可以。只需在远处选择一个点(对于工程师来说,这“足够接近”到无穷大)并开始向左折叠。最终,您最终在列表的开头。对于左折,情况并非如此,如果您在那儿选择一个点waaaay(并将其称为“足够接近”到列表的开头),然后向右开始折叠,那么您还有无限的路要走。

因此,诀窍在于,有时您无需达到无穷大。您可能甚至不需要在那里走走。但是您可能不知道需要走多远,在这种情况下,无限列表非常方便。

简单的例子是foldr (:) [] [1..]。让我们进行折叠。

回想一下foldr f z (x:xs) = f x (foldr f z xs)。在无限列表上,实际上什么都没关系,z所以我只是保留它z而不是[]使插图混乱

foldr (:) z (1:[2..])         ==> (:) 1 (foldr (:) z [2..])
1 : foldr (:) z (2:[3..])     ==> 1 : (:) 2 (foldr (:) z [3..])
1 : 2 : foldr (:) z (3:[4..]) ==> 1 : 2 : (:) 3 (foldr (:) z [4..])
1 : 2 : 3 : ( lazily evaluated thunk - foldr (:) z [4..] )

看看foldr,尽管理论上是从右折起,但在这种情况下,实际上是如何从左侧开始将结果列表中的各个元素分解出来的?因此,如果您take 3从此列表中可以清楚地看到,它将能够生产[1,2,3]并且无需进一步评估折痕。


7
这里的“关键点”是真正的关键,而不仅仅是说明,因为有问题的“某点”只是您实际使用的最远点。因此,实际上我们可以将“无穷大”定义为“比我们最终需要的多至少一个”,我认为这是一个可以满足工程师数学家的定义。
CA McCann

12

请记住,在Haskell中,由于延迟计算,您可以使用无限列表。因此,即使`[1 ..]无限长,head [1..]也只是1和head $ map (+1) [1..]2。如果没有得到,请停下来玩一会儿。如果您明白了,请继续阅读...

我觉得你的困惑的部分是,foldlfoldr总是从一侧或另一侧开始,因此您无需花太多时间。

foldr 有一个非常简单的定义

 foldr _ z [] = z
 foldr f z (x:xs) = f x $ foldr f z xs

为什么这会在无限列表上终止,请尝试

 dumbFunc :: a -> b -> String
 dumbFunc _ _ = "always returns the same string"
 testFold = foldr dumbFunc 0 [1..]

在这里,我们传入foldr“”(因为值无关紧要)和自然数的无限列表。这会终止吗?是。

它终止的原因是因为Haskell的评估等同于惰性术语重写。

所以

 testFold = foldr dumbFunc "" [1..]

变为(允许模式匹配)

 testFold = foldr dumbFunc "" (1:[2..])

与(根据我们对fold的定义)相同

 testFold = dumbFunc 1 $ foldr dumbFunc "" [2..]

现在,根据dumbFunc我们的定义,我们可以得出结论

 testFold = "always returns the same string"

当我们具有执行某些功能但有时很懒的功能时,这会更有趣。例如

foldr (||) False 

用于查找列表是否包含任何True元素。我们可以使用它来定义高阶函数anyTrue当且仅当传入的函数对于列表的某些元素为true时,该函数才返回

any :: (a -> Bool) -> [a] -> Bool
any f = (foldr (||) False) . (map f)

关于懒惰的评价的好处,就是当它遇到这将停止的第一元素e,使得f e == True

另一方面,并​​非如此foldl。为什么?foldl看起来真的很简单

foldl f z []     = z                  
foldl f z (x:xs) = foldl f (f z x) xs

现在,如果我们尝试上面的示例会发生什么

testFold' = foldl dumbFunc "" [1..]
testFold' = foldl dumbFunc "" (1:[2..])

现在变成:

testFold' = foldl dumbFunc (dumbFunc "" 1) [2..]

所以

testFold' = foldl dumbFunc (dumbFunc (dumbFunc "" 1) 2) [3..]
testFold' = foldl dumbFunc (dumbFunc (dumbFunc (dumbFunc "" 1) 2) 3) [4..]
testFold' = foldl dumbFunc (dumbFunc (dumbFunc (dumbFunc (dumbFunc "" 1) 2) 3) 4) [5..]

等等等等。我们永远无法到达任何地方,因为Haskell总是首先评估最外部的函数(简而言之就是惰性评估)。

这样的一个很酷的结果是,您可以实现foldlfoldr但反之则不能。这意味着以某种深刻的方式foldr是所有高阶字符串函数中最基础的,因为它是我们用来实现几乎所有其他函数的函数。您仍然可能需要使用foldl有时候,因为您可以foldl递归实现tail,并从中获得一些性能提升。


仍在阅读您的文章,并随即发表评论:这会终止于“假”值的无限列表吗?文件夹(||)错误。我可以看到它会如何在无限列表上终止,直到很久以前某个地方存在True,但是如果列表永远都是假的,那将导致不终止吗?
TheIronKnuckle

是的,我只是在ghci中运行了它(应该首先在:P中完成它)。它并没有终止
TheIronKnuckle

很高兴我的代码是正确的。它不会终止的事实是有道理的。如果您一次给我一张清单,我只能告诉您到目前为止它是否具有任何真实值,而不能告诉将来。因此,判断列表是否包含真实元素的最快方法是遍历并在第一个元素处停止,这就是foldr代码的作用,显然,如果找不到第一个元素,“在第一个元素处停止”不会终止...
菲利普·JF

0

Haskell Wiki上有很好的解释。它显示了使用不同类型的折叠和累加器功能的逐步还原。


-3

您的理解是正确的。我想知道作者是否正在尝试谈论Haskell的惰性评估系统(在该系统中,您可以将无限列表传递给不包括fold的各种函数,并且它将仅评估返回答案所需的内容)。但是我同意您的观点,即作者在描述该段中的内容时做得不好,并且说错了。


4
为什么不包括折叠?试试这个:foldr (:) [] [1..]
n。代词

2
否。该段可能不清楚,但是它说的没错。它在谈论惰性评估,给出的理由恰恰是为什么foldr实际上可以在无限列表上工作的原因。
CA McCann

愚蠢的事情是,如果我一直不停地阅读这本书,那以后我只会遇到解释:p
TheIronKnuckle
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.