foldr如何工作?

时间:2009-11-18 17:36:04

标签: haskell combinators fold

有人可以解释foldr的工作原理吗?

采取以下示例:

Prelude> foldr (-) 54 [10, 11]
53
Prelude> foldr (\x y -> (x+y)/2) 54 [12, 4, 10, 6]
12.0

我对这些处决感到困惑。有什么建议吗?

10 个答案:

答案 0 :(得分:114)

理解折叠器的最简单方法是重写你没有糖折叠的列表。

[1,2,3,4,5] => 1:(2:(3:(4:(5:[]))))

现在foldr f x所做的是,它将每个:替换为中缀形式的f,将[]替换为x并评估结果。

例如:

sum [1,2,3] = foldr (+) 0 [1,2,3]

[1,2,3] === 1:(2:(3:[]))

所以

sum [1,2,3] === 1+(2+(3+0)) = 6

答案 1 :(得分:72)

foldr从列表的右端开始,使用您提供的函数将每个列表条目与累加器值组合在一起。结果是在所有列表元素中“折叠”之后累加器的最终值。它的类型是:

foldr :: (a -> b -> b) -> b -> [a] -> b

从中您可以看到list元素(类型a)是给定函数的第一个参数,而累加器(类型b)是第二个。

第一个例子:

Starting accumulator = 54
11 -   54  = -43
10 - (-43) =  53

        ^  Result from the previous line

 ^ Next list item

所以你得到的答案是53。

第二个例子:

Starting accumulator = 54
(6  + 54) / 2 = 30
(10 + 30) / 2 = 20
(4  + 20) / 2 = 12
(12 + 12) / 2 = 12

结果是12.

编辑:我想添加,这是有限列表。 foldr也可以在无限列表上工作,但我认为最好首先考虑有限的情况。

答案 2 :(得分:37)

有助于理解foldrfoldl之间的区别。为什么foldr被称为“向右折叠”?

最初我认为这是因为它从右到左消耗了元素。但foldrfoldl从左到右都使用了列表。

  • foldl 从左到右评估(左联想)
  • foldr 从右到左评估(右关联)

我们可以通过使用关联性很重要的运算符的示例来明确区分。我们可以使用人类的例子,比如操作员,“吃”:

foodChain = (human : (shark : (fish : (algae : []))))

foldl step [] foodChain
  where step eater food = eater `eats` food  -- note that "eater" is the accumulator and "food" is the element

foldl `eats` [] (human : (shark : (fish : (algae : []))))
  == foldl eats (human `eats` shark)                              (fish : (algae : []))
  == foldl eats ((human `eats` shark) `eats` fish)                (algae : [])
  == foldl eats (((human `eats` shark) `eats` fish) `eats` algae) []
  ==            (((human `eats` shark) `eats` fish) `eats` algae)

这个foldl的语义是:一个人吃掉一些鲨鱼,然后吃了鲨鱼的同一个人然后吃了一些鱼等。食者就是蓄物。

与此对比:

foldr step [] foodChain
    where step food eater = eater `eats` food.   -- note that "eater" is the element and "food" is the accumulator

foldr `eats` [] (human : (shark : (fish : (algae : []))))
  == foldr eats (human `eats` shark)                              (fish : (algae : []))))
  == foldr eats (human `eats` (shark `eats` (fish))               (algae : [])
  == foldr eats (human `eats` (shark `eats` (fish `eats` algae))) []
  ==            (human `eats` (shark `eats` (fish `eats` algae) 

这个foldr的语义是:一个人吃了一条已经吃过鱼的鲨鱼,它已经吃了一些藻类。食物是蓄物。

foldlfoldr从左到右“剥离”食者,所以这不是我们将foldl称为“左折”的原因。相反,评估的顺序很重要。

答案 3 :(得分:20)

考虑foldr非常definition

 -- if the list is empty, the result is the initial value z
 foldr f z []     = z                  
 -- if not, apply f to the first element and the result of folding the rest 
 foldr f z (x:xs) = f x (foldr f z xs)

因此,例如foldr (-) 54 [10,11]必须等于(-) 10 (foldr (-) 54 [11]),即再次展开,等于(-) 10 ((-) 11 54)。所以内部操作是11 - 54,即-43;并且外部操作为10 - (-43),即10 + 43,因此您观察53。对你的第二个案例进行类似的步骤,你将再次看到结果如何形成!

答案 4 :(得分:13)

foldr表示从右侧折叠,因此foldr (-) 0 [1, 2, 3]生成(1 - (2 - (3 - 0)))。相比之下foldl生成(((0 - 1) - 2) - 3)

当运算符不是commutative foldl时,foldr会得到不同的结果。

在您的情况下,第一个示例扩展为(10 - (11 - 54)),后者为53。

答案 5 :(得分:6)

理解foldr的一种简单方法是:它用所提供功能的应用程序替换每个列表构造函数。您的第一个示例将转换为:

10 - (11 - 54)

从:

10 : (11 : [])

我从Haskell Wikibook获得的一条很好的建议可能在这里有用:

  

作为规则,您应该在可能无限的列表上使用foldr,或者在折叠构建数据结构的位置使用foldl',如果已知列表是有限的,则foldl归结为单一价值。 <{1}}(没有勾号)应该很少使用。

答案 6 :(得分:5)

我一直认为http://foldr.com是一个有趣的插图。请参阅Lambda the Ultimate帖子。

答案 7 :(得分:2)

我认为以简单的方式实现map,foldl和foldr有助于解释它们的工作原理。工作的例子也有助于我们的理解。

  myMap f [] = []
  myMap f (x:xs) = f x : myMap f xs

  myFoldL f i [] = i
  myFoldL f i (x:xs) = myFoldL f (f i x) xs

  > tail [1,2,3,4] ==> [2,3,4]
  > last [1,2,3,4] ==> 4
  > head [1,2,3,4] ==> 1
  > init [1,2,3,4] ==> [1,2,3]

  -- where f is a function,
  --  acc is an accumulator which is given initially
  --  l is a list.
  --
  myFoldR' f acc [] = acc
  myFoldR' f acc l = myFoldR' f (f acc (last l)) (init l)

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

  > map (\x -> x/2) [12,4,10,6] ==> [6.0,2.0,5.0,3.0]
  > myMap (\x -> x/2) [12,4,10,6] ==> [6.0,2.0,5.0,3.0]

  > foldl (\x y -> (x+y)/2) 54 [12, 4, 10, 6] ==> 10.125
  > myFoldL (\x y -> (x+y)/2) 54 [12, 4, 10, 6] ==> 10.125

    foldl from above: Starting accumulator = 54
      (12  + 54) / 2 = 33
      (4 + 33) / 2 = 18.5
      (10  + 18.5) / 2 = 14.25
      (6 + 14.25) / 2 = 10.125`

 > foldr (++) "5" ["1", "2", "3", "4"] ==> "12345"

 > foldl (++) "5" ["1", "2", "3", "4"] ==> “51234"

 > foldr (\x y -> (x+y)/2) 54 [12,4,10,6] ==> 12
 > myFoldR' (\x y -> (x+y)/2) 54 [12,4,10,6] ==> 12
 > myFoldR (\x y -> (x+y)/2) 54 [12,4,10,6] ==> 12

    foldr from above: Starting accumulator = 54
        (6  + 54) / 2 = 30
        (10 + 30) / 2 = 20
        (4  + 20) / 2 = 12
        (12 + 12) / 2 = 12

答案 8 :(得分:1)

好的,让我们看看参数:

  • 一个函数(它获取与它返回的值相同类型的列表元素和值(可能的部分结果));
  • 空列表特例的初始结果的说明
  • 一个清单;

返回值:

  • 一些最终结果

它首先将函数应用于列表中的最后一个元素和空列表结果。然后它使用此结果和前一个元素重新应用该函数,依此类推,直到它获取一些当前结果并且列表的第一个元素返回最终结果。

使用带有元素和一些先前折叠结果的函数,在初始结果周围折叠“折叠”列表。它为每个元素重复这个。因此,foldr从列表末尾开始,或从右侧开始。

folr f emptyresult [1,2,3,4]变成了 f(1, f(2, f(3, f(4, emptyresult) ) ) ) 。现在只需在评估中遵循括号即可。

需要注意的一件重要事情是,提供的函数f必须处理自己的返回值作为其第二个参数,这意味着它们必须具有相同的类型。

来源:my post如果您认为它可能会有帮助,我会从命令式的未经审核的javascript视角看待它。

答案 9 :(得分:1)

仔细阅读此处提供的其他答案以及它们之间的比较应该已经清楚了这一点,但是值得注意的是,接受的答案可能会误导初学者。正如其他评论者所指出的那样,在Haskell中执行的计算文件夹并不“位于列表的右端”;否则,foldr永远无法在无限列表上工作(在适当的条件下,它在Haskell中也是如此)。

Haskell的foldr函数的source code应该清楚:

foldr k z = go
          where
            go []     = z
            go (y:ys) = y `k` go ys

每个递归计算将最左边的原子列表项与列表尾部的递归计算结合在一起,即:

a\[1\] `f` (a[2] `f` (a[3] `f` ... (a[n-1] `f` a[n])  ...))

其中a[n]是初始累加器。

因为减少是“在Haskell中懒惰完成的”,所以它实际上是从左侧开始的。这就是我们所说的“惰性评估”,这是Haskell的显着特征。这对于理解Haskell foldr的操作很重要;因为实际上foldr从左侧递归建立并递减 计算,可以短路的二进制运算符具有机会,允许在适当的情况下将foldr减少无限个列表。

对于初学者来说,rl中的foldr(“右”)和foldl(“左”)将减少混乱。请参阅右关联性左关联性,要么保留它,要么尝试解释Haskell惰性评估机制的含义。

要研究您的示例,请遵循foldr源代码,我们建立以下表达式:

Prelude> foldr (-) 54 [10, 11]

->

10 - [11 - 54] = 53

再次:

foldr (\x y -> (x + y) / 2) 54 [12, 4, 10, 6]

->

(12 + (4 + (10 + (6 + 54) / 2) / 2) / 2) / 2 = 12