我对Learn You A Haskell的以下段落有疑问(好书imo,不要贬低它):
一个很大的不同就是正确 折叠在无限列表上工作,而左边没有!说得好 显而易见,如果你在某个时刻采用无限列表并将其折叠起来 从右边开始,你最终会到达列表的开头。 但是,如果您在某个点上获取无限列表并尝试折叠 它从左边开始,你永远不会结束!
我只是不明白这一点。如果你拿一个无限的列表并试图从右边折叠起来那么你将不得不从无穷远点开始,这就是没有发生(如果有人知道你能做到这一点的语言,请告诉:p )。至少,你必须根据Haskell的实现开始那里,因为在Haskell中,foldr和foldl不会采用一个参数来确定列表中应该开始折叠的位置。
我同意引用iff foldr和foldl接受确定列表中应该开始折叠的位置的参数,因为如果你采用无限列表并从定义的索引开始向右折叠它将是有意义的最终会终止,而从左侧折叠处开始并不重要;你将向无限折叠。但是,foldr和foldl 不采用这个参数,因此引用没有意义。在Haskell中,无限列表上的左侧折叠和右侧折叠都不会终止。
我的理解是正确还是我错过了什么?
答案 0 :(得分:84)
这里的关键是懒惰。如果您用于折叠列表的函数是严格的,那么给定无限列表,左侧折叠和右侧折叠都不会终止。
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 :(得分:17)
关键词是“在某些时候”。
如果您在某个位置获取无限列表并从右侧向上折叠,则最终会到达列表的开头。
所以你是对的,你不可能从无限列表的“最后”元素开始。但作者的观点是这样的:假设你可以。只需选择一个远在那里的点(对于工程师来说,这是“足够接近”到无限远)并开始向左折叠。最终你最终会在列表的开头。左侧折叠也是如此,如果你在那里选择一个点(并将其称为“足够接近”列表的开头),并开始向右折叠,你仍然有一个无限的方式去。
所以诀窍是,有时你不需要去无限。你甚至可能不需要去那里等待。但是你可能不知道你需要预先走多远,在这种情况下,无限列表非常方便。
简单的插图是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]
并且无需再评估折叠。
答案 2 :(得分:11)
请记住,在Haskell中,由于懒惰的评估,您可以使用无限列表。所以,head [1..]
只是1,head $ map (+1) [1..]
是2,即使`[1 ..]无限长。如果你不这样做,停下来玩一会儿。如果你这样做,请继续阅读......
我认为你的一部分困惑是foldl
和foldr
总是从一边开始,因此你不需要给出长度。
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
a“”(因为值无关紧要)和无限的自然数列表。这会终止吗?是。
它终止的原因是因为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
元素。我们可以使用它来定义更高阶的函数any
,当且仅当传入的函数对于列表的某个元素为真时才返回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总是首先评估最外层的函数(简而言之就是懒惰的评估)。
这样做的一个很好的结果是,您可以从foldl
中实现foldr
,但反之亦然。这意味着以某种深刻的方式foldr
是所有高阶字符串函数中最基本的函数,因为它是我们用来实现几乎所有其他函数的函数。您有时可能还想使用foldl
,因为可以递归地实现foldl
尾,并从中获得一些性能提升。
答案 3 :(得分:0)
Haskell wiki有很好的解释。它显示了使用不同类型的折叠和累加器功能逐步减少。
答案 4 :(得分:-3)
您的理解是正确的。我想知道作者是否正试图讨论Haskell的惰性评估系统(在其中你可以将无限列表传递给不包括fold的各种函数,并且它只会评估返回答案所需要的很多)。但我同意你的观点,即作者在描述该段落中的任何内容方面并没有做得很好,而且说的是错误的。