我们被要求回答foldr
或foldl
是否更有效。
我不确定,但这不取决于我在做什么,特别是我希望通过我的功能达到什么目标?
各个案例之间是否存在差异,或者可以说foldr
或foldl
更好,因为......
有一般答案吗?
提前致谢!
答案 0 :(得分:33)
关于这个问题的一个相当规范的来源是Haskell Wiki上的Foldr Foldl Foldl'。总之,根据您对列表元素的组合以及弃牌结果的严格程度,您可以决定选择foldr
或foldl'
。选择foldl
很少是正确的选择。
通常,这是一个很好的例子,说明如何在Haskell中有效地计算函数的懒惰和严格性。在严格的语言中,尾递归定义和TCO是游戏的名称,但对于Haskell而言,这些定义可能过于“无效”(不够懒惰)导致产生无用的thunk并且优化的机会也更少。
foldr
如果使用的操作你的折叠结果可以懒得运行,而你的组合函数在其正确的参数中是非严格的,那么foldr
通常是正确的选择。这方面的典型例子是nonfold
。首先,我们看到(:)
在右边是非严格的
head (1 : undefined)
1
然后使用nonfold
foldr
nonfoldr :: [a] -> [a]
nonfoldr = foldr (:) []
由于(:)
懒惰地创建列表,因此像head . nonfoldr
这样的表达式非常有效,只需要一个折叠步骤并且只强制输入列表的头部。
head (nonfoldr [1,2,3])
head (foldr (:) [] [1,2,3])
head (1 : foldr (:) [] [2,3])
1
懒惰胜出的一个非常常见的地方是短路计算。例如,lookup :: Eq a => a -> [a] -> Bool
可以通过返回看到匹配的时刻来提高效率。
lookupr :: Eq a => a -> [a] -> Bool
lookupr x = foldr (\y inRest -> if x == y then True else inRest) False
发生短路是因为我们在isRest
的第一个分支中丢弃if
。 foldl'
中实现的相同内容无法做到这一点。
lookupl :: Eq a => a -> [a] -> Bool
lookupl x = foldl' (\wasHere y -> if wasHere then wasHere else x == y) False
lookupr 1 [1,2,3,4]
foldr fn False [1,2,3,4]
if 1 == 1 then True else (foldr fn False [2,3,4])
True
lookupl 1 [1,2,3,4]
foldl' fn False [1,2,3,4]
foldl' fn True [2,3,4]
foldl' fn True [3,4]
foldl' fn True [4]
foldl' fn True []
True
foldl'
如果消费操作或组合需要在可以继续之前处理整个列表,那么foldl'
通常是正确的选择。通常,对这种情况的最佳检查是问问自己你的组合功能是否严格 - 如果在第一个参数中它是严格的那么你的整个列表必须被强制。这方面的典型例子是sum
sum :: Num a => [a] -> a
sum = foldl' (+) 0
因为(1 + 2)
在实际添加之前无法合理消耗(Haskell不够聪明,在没有先评估1 + 2 >= 1
的情况下知道1 + 2
),所以我们没有得到任何使用foldr
可以获益。相反,我们将使用foldl'
的严格组合属性来确保我们根据需要急切地评估事物
sum [1,2,3]
foldl' (+) 0 [1,2,3]
foldl' (+) 1 [2,3]
foldl' (+) 3 [3]
foldl' (+) 6 []
6
请注意,如果我们在这里选择foldl
,我们就无法获得正确的结果。虽然foldl
与foldl'
具有相同的关联性,但它并不强制seq
与foldl'
相关的合并操作。
sumWrong :: Num a => [a] -> a
sumWrong = foldl (+) 0
sumWrong [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
如果我们在foldr
最佳位置选择foldl
或foldl'
,我们会获得额外的,无用的thunks(空间泄漏),如果我们选择,我们会得到额外的,无用的评估(时间泄漏)当foldl'
成为更好的选择时foldr
。
nonfoldl :: [a] -> [a]
nonfoldl = foldl (:) []
head (nonfoldl [1,2,3])
head (foldl (:) [] [1,2,3])
head (foldl (:) [1] [2,3])
head (foldl (:) [1,2] [3]) -- nonfoldr finished here, O(1)
head (foldl (:) [1,2,3] [])
head [1,2,3]
1 -- this is O(n)
sumR :: Num a => [a] -> a
sumR = foldr (+) 0
sumR [1,2,3]
foldr (+) 0 [1,2,3]
1 + foldr (+) 0 [2, 3] -- thunks begin
1 + (2 + foldr (+) 0 [3])
1 + (2 + (3 + foldr (+) 0)) -- O(n) thunks hanging about
1 + (2 + (3 + 0)))
1 + (2 + 3)
1 + 5
6 -- forced O(n) thunks
答案 1 :(得分:4)
在具有严格/急切评估的语言中,从左侧折叠可以在恒定空间中完成,而从右侧折叠需要线性空间(在列表的元素数量上)。因此,许多首先接近Haskell的人都会接受这种先入之见。
但是由于懒惰的评价,那个经验法则在Haskell 中不起作用。在Haskell中可以使用foldr
编写常量空间函数。这是一个例子:
find :: (a -> Bool) -> [a] -> Maybe a
find p = foldr (\x next -> if p x then Just x else next) Nothing
让我们尝试手工评估find even [1, 3, 4]
:
-- The definition of foldr, for reference:
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
find even (1:3:4:[])
= foldr (\x next -> if even x then Just x else next) (1:3:4:[])
= if even 1 then Just 1 else foldr (\x next -> if even x then Just x else next) (3:4:[])
= foldr (\x next -> if even x then Just x else next) (3:4:[])
= if even 3 then Just 3 else foldr (\x next -> if even x then Just x else next) (4:[])
= foldr (\x next -> if even x then Just x else next) (4:[])
= if even 4 then Just 4 else foldr (\x next -> if even x then Just x else next) []
= Just 4
中间步骤中表达式的大小具有恒定的上限 - 这实际上意味着此评估可以在恒定的空间中执行。
Haskell中的foldr
可以在恒定空间中运行的另一个原因是list fusion optimizations in GHC。在许多情况下,GHC可以将foldr
优化为恒定空间生成器上的恒定空间循环。对于左侧折叠,通常不能这样做。
尽管如此,Haskell 中的左侧折叠可以编写以使用尾递归,可以导致性能优势。事实上,为了实现这一目标,你需要非常小心懒惰 - 天真地尝试编写尾递归算法通常会导致线性空间执行,因为未经评估的表达式会积累。
外卖课程:
Prelude
和Data.List
中的库函数,因为它们已经过仔细编写以利用列表融合等性能功能。 / LI>
foldr
。foldl
,请使用foldl'
(避免未评估表达式的版本)。答案 2 :(得分:1)
(请阅读这篇文章的评论。一些有趣的观点和我在这里写的内容并不完全正确!)
这取决于。 foldl通常更快,因为它的尾递归,意思是(有点),所有计算都是就地完成的,并且没有调用堆栈。供参考:
foldl f a [] = a
foldl f a (x:xs) = foldl f (f a x) xs
要运行foldr,我们需要一个调用堆栈,因为f
有一个“待定”计算。
foldr f a [] = a
foldr f a (x:xs) = f x (foldr f a xs)
另一方面,如果f在其第一个参数中不严格,则foldr可能会短路。它在某种程度上是 lazier 。例如,如果我们定义新产品
prod 0 x = 0
prod x 0 = 0
prod x y = x*y
然后
foldr prod 1 [0...n]
在n中占用恒定时间,但
foldl prod 1 [0...n]
需要线性时间。 (这不会使用(*)
,因为它不检查任何参数是否为0.所以我们创建一个非严格的版本。感谢Ingo和Daniel Lyons在评论中指出它)