我想测试foldl vs foldr.从我所看到的你应该使用foldl过foldr时,你永远可以由于尾reccursion优化。

这是有道理的。然而,在运行这个测试我很困惑:

foldr(需要0.057s当使用时命令):

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

main = putStrLn(show ( sum (foldr a [] [0.. 100000])))

foldl(需要0.089s当使用时命令):

b::[b] -> b -> [b]
b xs = ( ++ xs). (\y->[y])

main = putStrLn(show ( sum (foldl b [] [0.. 100000])))

很明显,这个例子是微不足道,但是我困惑为什么foldr是打foldl.不这是一个明确的情况下foldl赢了?

有帮助吗?

解决方案

欢迎到惰性计算的世界。

当你想想看,严格的评估,与foldl看起来“好”的条款和foldr相似容貌的“坏”,因为与foldl是尾递归,但foldr相似本来在堆栈中建立一个塔,因此它可以先处理的最后一个项目

然而,惰性计算转动表。举个例子来说,所述映射函数的定义:

map :: (a -> b) -> [a] -> [b]
map _ []     = []
map f (x:xs) = f x : map f xs

这不会是如果哈斯克尔使用严格的评价不太好,因为它必须先计算尾,然后前面加上项目(列表中的所有项目)。只有这样,才能做到这一点有效地将构建在反向元件,它似乎。

然而,由于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)

由于由f限定的map可以返回仅使用第一参数的结果列表的第一项,所述折叠可在懒惰地恒定空间中操作。

现在,惰性计算确实反咬。例如,尝试运行总和[1..1000000。它产生一个堆栈溢出。何必呢?它应该只是由左到右,右?

评估

让我们看看哈斯克尔是如何评价它:

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的塔结束。该评价过程中发生堆栈溢出,因为它必须深深递归来评估所有的thunk。

幸运的是,在称为Data.List模块foldl'一个特殊功能,严格操作。 foldl' (+) 0 [1..1000000]不会堆栈溢出。 (注:我想在您的测试与foldl更换foldl',但它实际上使它运行速度较慢。)

其他提示

编辑:在寻找这个问题再一次,我认为目前所有的解释稍有不足,所以我已经写了一个较长的解释。

所不同的是,在如何 foldlfoldr 运用他们的减少功能。看 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 功能:

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版本。

这也是一个情况下 foldl' 不会的帮助。加严格的 foldl' 不会改变的方式中列出创建。头部的名单仍然无法使用,直到foldl'已经完成,因此结果将仍然是比较慢 foldr.

我用下列规则来确定最好的选择 fold

  • 为褶皱的是一个 减少, 使用 foldl' (例如这将是唯/最终遍历)
  • 否则使用 foldr.
  • 不要用 foldl.

在大多数情况下 foldr 是最好的折功能,因为穿越方向是最佳的懒惰的评价的清单。它也是唯一一个有能力处理无限的清单。额外的严格的 foldl' 可以使它更快地在一些情况,但这取决于你怎么会使用结构和多懒惰。

我不认为任何人居然说在这一个真正的答案,除非我失去了一些东西(这很可能是真实的,并与downvotes欢迎)。

我认为在这种情况下,最大的不同之处在于foldr建立这样的列表:

[0] ++([1] ++([2] ++(++ ... [百万])))

尽管foldl构建列表是这样的:

((([0] ++ [1])++ [2])++ ...)++ [999888])++ [999999])++ [百万]

在细微的差别,但请注意,在foldr版本++始终只有一个列表元素作为其左边的参数。与foldl版本,有高达++的左参数999999个元件(平均约500000),但是只有一个元素在右边的参数。

然而,++需要时间正比于左参数的大小,因为它看起来虽然整个左参数列表的末尾,然后将该最后一个元素重新指向到右边的参数的第一个元素(在最好的,也许它实际上需要做的复印件)。正确的参数列表是不变的,所以它不会不管它是多么大了。

这就是为什么foldl版本要慢得多。它什么都没有做懒惰在我看来。

问题是,尾递归化是一个存优化,不是一个执行时优化!

尾递归化可避免的需要记住值为每递归的呼吁。

因此,foldl是在事实上"好"的和foldr是"坏"。

例如,考虑到定义的foldr和foldl:

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]
foldl (+) (((0+1)+2)+3) [ ]
(((0+1)+2)+3)
((1+2)+3)
(3+3)
6

注意foldl不记得值0,1,2...,但是通过个整体的表达(((0+1)+2)+3) 作为论点,懒洋洋地以及不会评估它直到最后评价foldl,它达到基本情况和返回的价值,通过了作为第二个参数(z)至极不是进行评估。

另一方面,这就是如何foldr工作:

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

重要的差别这里是哪里foldl评估整体的表达的最后一个电话,避免需要回到达记住值,foldr没有。foldr记住一整数的每个呼吁并执行一个外在每个呼吁。

重要的是要铭记,foldr和foldl并不总是等同物。例如,尝试计算这表现在拥抱:

foldr (&&) True (False:(repeat True))

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

foldr和foldl是相当只在某些条件的描述 在这里,

(对不起我英语不好)

有关一个,所述列表[0.. 100000]需要扩大马上使得foldr相似可以与最后一个元素开始。然后,当它折叠东西放在一起,中间结果

[100000]
[99999, 100000]
[99998, 99999, 100000]
...
[0.. 100000] -- i.e., the original list

由于没有人被允许改变该列表的值(Haskell是纯功能性的语言),编译器可以自由地再次使用该值。中间值,象[99999, 100000]甚至可以简单指针到膨胀[0.. 100000]列表,而不是单独的列表。

有关B,看中间值:

[0]
[0, 1]
[0, 1, 2]
...
[0, 1, ..., 99999]
[0.. 100000]

每个那些中间的列表中不能再用,因为如果你改变了列表的末尾,那么你已经改变了任何其他值指向它。所以,你要创建一个一堆需要时间来建立在内存中额外的名单。因此,在这种情况下,你花更多的时间分配和在这些名单是中间值填充。

既然你只是把名单副本,一个运行速度更快,因为它开始通过扩展的完整列表,然后只是不断从列表中的回前方移动的指针。

无论foldl也不foldr是尾优化。它只是foldl'

但在使用++foldl'你的情况是不是好主意,因为++的连续评估将引起一次又一次穿越增长累加器。

好了,让我重写的方式你的函数的区别应该是显而易见的 -

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

b :: [b] -> b -> [b]
b = flip (:)

您看到,b比更复杂。如果你想成为精确a需要针对要计算的值减少一个步骤,但b需要两个。这使得要测量的时间差,在第二个例子中的两倍减少必须执行的。

//编辑:但是时间复杂度是一样的,所以我不会理会太多

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top