Optimization foldl是尾部递归的,那么为什么foldr比foldl运行得更快呢?

Optimization foldl是尾部递归的,那么为什么foldr比foldl运行得更快呢?,optimization,haskell,tail-recursion,combinators,fold,Optimization,Haskell,Tail Recursion,Combinators,Fold,我想测试foldl和foldr。从我所看到的情况来看,由于尾部递归优化,您应该尽可能使用foldl而不是foldr 这是有道理的。但是,运行此测试后,我感到困惑: foldr(使用time命令时需要0.057秒): foldl(使用time命令时需要0.089秒): 很明显,这个例子并不重要,但我不明白为什么foldr要打败foldl。这不是foldl获胜的一个明显的例子吗?好吧,让我用一种明显不同的方式重写你的函数- a :: a -> [a] -> [a] a = (:) b

我想测试foldl和foldr。从我所看到的情况来看,由于尾部递归优化,您应该尽可能使用foldl而不是foldr

这是有道理的。但是,运行此测试后,我感到困惑:

foldr(使用time命令时需要0.057秒):

foldl(使用time命令时需要0.089秒):


很明显,这个例子并不重要,但我不明白为什么foldr要打败foldl。这不是foldl获胜的一个明显的例子吗?

好吧,让我用一种明显不同的方式重写你的函数-

a :: a -> [a] -> [a]
a = (:)

b :: [b] -> b -> [b]
b = flip (:)
你看b比a更复杂。如果要精确,则
a
需要一个缩减步骤来计算值,而
b
需要两个缩减步骤。在第二个例子中,这使得您测量的时差必须减少两倍


//编辑:但是时间复杂度是一样的,所以我不会太在意它。

欢迎来到懒惰评估的世界

当您从严格的评估角度考虑它时,foldl看起来“好”,而foldr看起来“坏”,因为foldl是尾部递归的,但是foldr必须在堆栈中构建一个塔,以便它可以首先处理最后一项

然而,懒惰的评估改变了局面。以map函数的定义为例:

map :: (a -> b) -> [a] -> [b]
map _ []     = []
map f (x:xs) = f x : map f xs
b xs = ( ++xs) . (\y->[y])
foldl b [] [0..10000]
foldl b ( [0] ++ [] ) [1..10000]
foldl b ( [1] ++ ([0] ++ []) ) [2..10000]
foldl b ( [2] ++ ([1] ++ ([0] ++ [])) ) [3..10000]
...
如果Haskell使用严格的求值,这就不太好了,因为它必须首先计算尾部,然后预结束项(对于列表中的所有项)。看来,唯一有效的方法是反向构建元素

然而,由于Haskell的惰性计算,这个映射函数实际上是有效的。Haskell中的列表可以看作是生成器,此映射函数通过将f应用于输入列表的第一项来生成其第一项。当它需要第二项时,它只需再次执行相同的操作(不使用额外的空间)

事实证明,
map
可以用
foldr
来描述:

map f xs = foldr (\x ys -> f x : ys) [] xs
通过查看它很难判断,但是懒惰的评估开始了,因为foldr可以立即给出
f
它的第一个参数:

foldr f z []     = z
foldr f z (x:xs) = f x (foldr f z xs)
由于由
map
定义的
f
可以仅使用第一个参数返回结果列表的第一项,因此折叠可以在恒定空间中惰性地操作

现在,懒惰的评估确实起到了反作用。例如,尝试运行sum[1..1000000]。它产生堆栈溢出。为什么要这样做?它应该从左到右计算,对吗

让我们看看Haskell是如何评估它的:

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

sum = foldl (+) 0

sum [1..1000000] = foldl (+) 0 [1..1000000]
                 = foldl (+) ((+) 0 1) [2..1000000]
                 = foldl (+) ((+) ((+) 0 1) 2) [3..1000000]
                 = foldl (+) ((+) ((+) ((+) 0 1) 2) 3) [4..1000000]
                   ...
                 = (+) ((+) ((+) (...) 999999) 1000000)
Haskell太懒了,无法执行添加操作。取而代之的是,它最终导致了一个未经评估的恶棍塔,这些恶棍不得不被迫获取一个数字。堆栈溢出发生在该求值过程中,因为它必须深入递归以求值所有thunk


幸运的是,Data.List中有一个名为
foldl'
的特殊函数,它的操作非常严格
foldl'(+)0[1..1000000]
不会堆栈溢出。(注意:在您的测试中,我尝试用
foldl'
替换
foldl
,但实际上它使运行速度变慢。)

无论
foldl
还是
foldr
都不是尾部优化的。它只是
foldl'


但是在您的情况下,将
++
foldl'
一起使用不是一个好主意,因为对
++
的连续求值将导致一次又一次遍历不断增长的累加器。

对于a,需要立即展开
[0..100000]
列表,以便foldr可以从最后一个元素开始。然后当它把东西折叠在一起时,中间的结果是

[100000]
[99999, 100000]
[99998, 99999, 100000]
...
[0.. 100000] -- i.e., the original list
由于不允许任何人更改此列表值(Haskell是一种纯函数语言),因此编译器可以自由地重用该值。中间值,如
[999999,100000]
甚至可以简单地指向扩展的
[0..100000]
列表,而不是单独的列表

对于b,请查看中间值:

[0]
[0, 1]
[0, 1, 2]
...
[0, 1, ..., 99999]
[0.. 100000]
这些中间列表中的每一个都不能被重用,因为如果您更改了列表的结尾,那么您就更改了指向它的任何其他值。因此,您正在创建一组额外的列表,这些列表需要时间在内存中构建。因此,在这种情况下,您需要花费更多的时间来分配和填写这些中间值列表


由于您只是创建列表的副本,a运行得更快,因为它首先扩展完整列表,然后一直将指针从列表的后面移到前面。

编辑:再次查看此问题时,我认为当前所有的解释都有点不足,因此我写了一个较长的解释

区别在于
foldl
foldr
如何应用它们的缩减功能。查看
foldr
案例,我们可以将其扩展为

foldr (\x -> [x] ++ ) [] [0..10000]
[0] ++ foldr a [] [1..10000]
[0] ++ ([1] ++ foldr a [] [2..10000])
...
此列表由
sum
处理,其使用方式如下:

sum = foldl' (+) 0
foldl' (+) 0 ([0] ++ ([1] ++ ... ++ [10000]))
foldl' (+) 0 (0 : [1] ++ ... ++ [10000])     -- get head of list from '++' definition
foldl' (+) 0 ([1] ++ [2] ++ ... ++ [10000])  -- add accumulator and head of list
foldl' (+) 0 (1 : [2] ++ ... ++ [10000])
foldl' (+) 1 ([2] ++ ... ++ [10000])
...
我省略了列表连接的细节,但这就是减少的过程。重要的一点是,为了最小化列表遍历,所有内容都得到了处理。
foldr
只遍历列表一次,串联不需要连续的列表遍历,
sum
最终在一次遍历中消耗列表。关键的是,列表的标题可以从
foldr
立即到
sum
,因此
sum
可以立即开始工作,并且可以在生成值时对其进行gc。使用诸如
vector
之类的融合框架,甚至中间列表也可能被融合掉

foldl
功能相比:

map :: (a -> b) -> [a] -> [b]
map _ []     = []
map f (x:xs) = f x : map f xs
b xs = ( ++xs) . (\y->[y])
foldl b [] [0..10000]
foldl b ( [0] ++ [] ) [1..10000]
foldl b ( [1] ++ ([0] ++ []) ) [2..10000]
foldl b ( [2] ++ ([1] ++ ([0] ++ [])) ) [3..10000]
...
请注意,现在列表的标题在
foldl
完成之前不可用。这意味着在
sum
开始工作之前,必须在内存中构建整个列表。总的来说,效率要低得多。使用
+RTS-s
运行这两个版本会显示
foldl f z [] = z
foldl f z (x:xs) = foldl f (z `f` x) xs

foldr f z [] = z
foldr f z (x:xs) = x `f` (foldr f z xs)
foldl (+) 0 [1, 2, 3]
foldl (+) (0+1) [2, 3]
foldl (+) ((0+1)+2) [3]
foldl (+) (((0+1)+2)+3) [ ]
(((0+1)+2)+3)
((1+2)+3)
(3+3)
6
foldr (+) 0 [1, 2, 3]
1 + (foldr (+) 0 [2, 3])
1 + (2 + (foldr (+) 0 [3]))
1 + (2 + (3 + (foldr (+) 0 [])))
1 + (2 + (3 + 0)))
1 + (2 + 3)
1 + 5
6
foldr (&&) True (False:(repeat True))

foldl (&&) True (False:(repeat True))