无限列表的左右折叠

Posted

技术标签:

【中文标题】无限列表的左右折叠【英文标题】:Left and Right Folding over an Infinite list 【发布时间】:2011-11-15 20:29:56 【问题描述】:

我对来自Learn You A Haskell 的以下段落有疑问(海事组织很棒的书,不是贬低它):

一个很大的不同是正确的 折叠在无限列表上工作,而左边的则不行!说起来 很明显,如果你在某个时候取出一个无限列表并将它折叠起来 从右侧开始,您最终将到达列表的开头。 但是,如果您在某个点上获取无限列表并尝试折叠 从左边往上走,你永远不会走到尽头!

我就是不明白。如果您采用无限列表并尝试从右侧折叠它,那么您将不得不从无穷大的点开始,这不会发生(如果有人知道您可以这样做的语言,请告诉:p )。至少,您必须根据 Haskell 的实现从那里开始,因为在 Haskell 中, foldr 和 foldl 不接受决定它们应该在列表中的哪个位置开始折叠的参数。

如果 foldr 和 foldl 采用了决定它们应该在列表中的哪个位置开始折叠的参数,我会同意引用,因为如果您采用无限列表并从定义的索引开始折叠它将是有意义的 最终终止,而从左折叠开始并不重要;你将向无穷大折叠。然而 foldr 和 foldl 接受这个论点,因此引用没有意义。在 Haskell 中,无限列表上的左折叠和右折叠都不会终止

我的理解是正确的还是我遗漏了什么?

【问题讨论】:

也许你应该看看相关专栏中的这个问题:***.com/questions/833186/… foldlfoldr both 从左到右处理列表。 This 可以帮助你更深入地理解折叠。 【参考方案1】:

这里的关键是懒惰。如果您用于折叠列表的函数是严格的,那么在给定无限列表的情况下,左折叠和右折叠都不会终止。

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 是否严格,我们永远无法到达该应用程序。

【讨论】:

这很有趣:foldr (\x y -> x) 0 [1..]。这真的是懒惰评估的例子吗?还是只是编译器很聪明?就像,我不知道如何以传统意义上的方式评估它,但很明显最终的价值是什么。那么 GHC 实际上是在懒惰地评估(如果是,那么 *** 如何?:P)或者它是否足够聪明地认识到答案永远是 1? @ThelronKnuckle 这是懒惰的评估。或者更确切地说,这是 Haskell 的非严格语义。编译器无论多么聪明都不允许改变它。 @ThelronKnuckle 这是惰性求值,除非编译器执行超级编译。 好吧,可能是懒惰的评估,但是在某些时候它必须被评估,那么到底发生了什么?它是否将“无穷大”粘在累加器中,然后向右折叠并在累加器中放置“无穷大 - 1”,然后将“无穷大 - 2”等等一直折叠到 1?或者 GHC 是否足够聪明,可以看到结果为 1,然后将其放入? @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【参考方案2】:

关键短语是“在某些时候”。

如果您在某个点取出无限列表并从右侧折叠它,您最终将到达列表的开头。

所以你是对的,你不可能从无限列表的“最后一个”元素开始。但作者的观点是:假设你可以。只需选择一个离那里很远的点(对于工程师来说,这“足够接近”到无穷大)并开始向左折叠。最终你会排在列表的开头。左折叠的情况并非如此,如果你选择一个点 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],并且不需要进一步评估折叠。

【讨论】:

“选择某个点并从右侧返回”部分确实是这里的关键,而不仅仅是一个插图——因为所讨论的“某个点”只是您实际使用的最远点。所以实际上我们可以将“无穷大”定义为“至少比我们最终需要的多一个”,我认为这是一个可以满足工程师数学家的定义。【参考方案3】:

请记住,在 Haskell 中,由于惰性求值,您可以使用无限列表。所以,head [1..] 只是 1,head $ map (+1) [1..] 是 2,即使 `[1..] 是无限长的。如果你不明白,停下来玩一会儿。如果你明白了,请继续阅读......

我认为您的部分困惑是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..])

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

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

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

 testFold = "always returns the same string"

当我们有功能做某事但有时很懒惰时,这会更有趣。例如

foldr (||) False 

用于查找列表是否包含任何True 元素。我们可以使用它来定义高阶函数nany,当且仅当传入的函数对于列表的某些元素为真时,它才会返回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 总是首先评估最外层的函数(简而言之就是惰性评估)。

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

【讨论】:

仍在阅读您的帖子,边走边评论:这会在无限的“假”值列表上终止吗?折叠 (||) 错误。我可以看到它如何在无限列表上终止,只要在某处有一个 True ,但如果列表永远为假,那将导致不终止,对吗? 是的,我只是通过 ghci 运行它(应该首先完成它:P)。它没有终止 很高兴我的代码是正确的。它不会终止的事实是有道理的。如果你一次给我一个清单,我只能说它到目前为止是否有任何真正的价值,而不是将来是否会有。所以判断一个列表是否有任何真正元素的最快方法是遍历并在第一个元素处停止,这就是foldr 代码所做的事情,显然“在第一个元素处停止”不会终止如果你找不到第一个……【参考方案4】:

Haskell wiki 上有很好的解释。它显示了使用不同类型的折叠和累加器函数逐步减少。

【讨论】:

【参考方案5】:

你的理解是正确的。我想知道作者是否试图谈论 Haskell 的惰性评估系统(在该系统中,您可以将无限列表传递给不包括折叠的各种函数,并且它只会评估返回答案所需的多少)。但我同意你的观点,作者没有很好地描述该段落中的任何内容,而且它所说的内容是错误的。

【讨论】:

为什么不包括折叠?试试这个:foldr (:) [] [1..] 没有。该段落可能不清楚,但没有任何内容是错误。它在谈论惰性评估,给出的理由正是为什么 foldr 实际上可以在无限列表上工作。 愚蠢的是,如果我一直读这本书,我会在几页之后遇到我想要的解释:p

以上是关于无限列表的左右折叠的主要内容,如果未能解决你的问题,请参考以下文章

无限深度和无限宽度玫瑰树的延迟折叠到其边缘路径

无限输入的非确定性

具有无限级别的可折叠导航菜单

JS实现无限级网页折叠菜单(类似树形菜单)

小程序实现无限瀑布流

React Material UI 多重折叠