折叠& foldl Haskell解释

时间:2013-12-03 16:40:11

标签: haskell

我们被要求回答foldrfoldl是否更有效。

我不确定,但这不取决于我在做什么,特别是我希望通过我的功能达到什么目标?

各个案例之间是否存在差异,或者可以说foldrfoldl更好,因为......

有一般答案吗?

提前致谢!

3 个答案:

答案 0 :(得分:33)

关于这个问题的一个相当规范的来源是Haskell Wiki上的Foldr Foldl Foldl'。总之,根据您对列表元素的组合以及弃牌结果的严格程度,您可以决定选择foldrfoldl'。选择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的第一个分支中丢弃iffoldl'中实现的相同内容无法做到这一点。

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,我们就无法获得正确的结果。虽然foldlfoldl'具有相同的关联性,但它并不强制seqfoldl'相关的合并操作。

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最佳位置选择foldlfoldl',我们会获得额外的,无用的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 中的左侧折叠可以编写以使用尾递归,可以导致性能优势。事实上,为了实现这一目标,你需要非常小心懒惰 - 天真地尝试编写尾递归算法通常会导致线性空间执行,因为未经评估的表达式会积累。

外卖课程:

  1. 当您开始使用Haskell时,尽可能尝试使用PreludeData.List中的库函数,因为它们已经过仔细编写以利用列表融合等性能功能。 / LI>
  2. 如果您需要折叠列表,请先尝试foldr
  3. 永远不要使用foldl,请使用foldl'(避免未评估表达式的版本)。
  4. 如果你想在Haskell中使用尾递归,首先你需要了解评估是如何工作的 - 否则你可能会让事情变得更糟。

答案 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在评论中指出它)