记住在 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
。这是否会终止呢?是的。
它能够终止的原因是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
any
用于检查列表中是否包含任何 True
元素。我们可以使用它来定义高阶函数 any
,如果传入的函数对列表中的某个元素返回真,则返回 True
。
any :: (a -> Bool) -> [a] -> Bool
any f = (foldr (||) False) . (map f)
惰性求值的好处在于,当它遇到第一个使得 f e == True
成立的元素 e
时就会停止。
然而,这并不适用于 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
和foldr
都 从左到右处理列表。这篇文章 可以帮助你更深入地理解 folds。 - Matthias Braun