【发布时间】:2010-12-17 23:34:42
【问题描述】:
谁能解释foldr 是如何工作的?
举个例子:
Prelude> foldr (-) 54 [10, 11]
53
Prelude> foldr (\x y -> (x+y)/2) 54 [12, 4, 10, 6]
12.0
我对这些处决感到困惑。有什么建议吗?
【问题讨论】:
标签: haskell combinators fold
谁能解释foldr 是如何工作的?
举个例子:
Prelude> foldr (-) 54 [10, 11]
53
Prelude> foldr (\x y -> (x+y)/2) 54 [12, 4, 10, 6]
12.0
我对这些处决感到困惑。有什么建议吗?
【问题讨论】:
标签: haskell combinators fold
理解 foldr 的最简单方法是重写你要折叠的列表,而不加糖。
[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
【讨论】:
[] 和 cons 运算符,带有您选择的累加器和函数。
foldr,因此用户可以决定如何处理“cons”和“零”案例
foldr 从列表的右端开始,并使用您提供的函数将每个列表条目与累加器值组合在一起。结果是累加器在所有列表元素中“折叠”后的最终值。它的类型是:
foldr :: (a -> b -> b) -> b -> [a] -> b
从中可以看出,列表元素(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 也可以在无限列表上工作,但我认为最好先了解有限情况。
【讨论】:
foldr 不是“从右边开始”。它是右关联的。您可以通过阅读Foldable 的[] 实例的源代码来验证hackage.haskell.org/package/base-4.10.0.0/docs/src/…
foldr 背后的所有理论和实现细节。我只需要简单解释一下foldr 是如何得到结果的。如果解释过于简化也没关系。 Haskell 与我学过的任何命令式或函数式语言都非常不同。我只需要充分了解foldr 就可以继续解决Haskell 99 问题。正如我所做的那样,我的头脑将更完整地了解 Haskell 的全部内容。感谢您让事情尽可能简单。
foldr (||) False [True, error "ERROR"] 必须导致错误,但它返回True。不,foldr 被定义为 foldr (||) z [] = z 和 foldr (||) z (x:xs) = x || foldr (||) z xs 就是这样。
这有助于理解foldr 和foldl 之间的区别。为什么foldr被称为“对折”?
最初我以为是因为它从右到左消耗元素。然而foldr 和foldl 都是从左到右使用列表的。
foldl 评估从左到右(左关联)foldr 评估从右到左(右关联)我们可以通过一个使用关联性很重要的运算符的示例来明确这种区别。我们可以使用一个人类示例,例如操作符“eats”:
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的语义是:一个人吃了一条已经吃过鱼的鲨鱼,而鱼已经吃了一些藻类。食物是蓄能器。
foldl 和 foldr 从左到右“剥离”食客,所以这不是我们将 foldl 称为“左折叠”的原因。相反,评估的顺序很重要。
【讨论】:
想想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。对您的第二种情况执行类似的步骤,您将再次看到结果如何形成!
【讨论】:
foldr f z t = appEndo (foldMap (Endo #. f) t) z
foldr 表示从右边折叠,所以foldr (-) 0 [1, 2, 3] 产生(1 - (2 - (3 - 0)))。相比之下,foldl 产生 (((0 - 1) - 2) - 3)。
当操作符不是commutativefoldl和foldr会得到不同的结果。
在您的情况下,第一个示例扩展为 (10 - (11 - 54)),得到 53。
【讨论】:
理解foldr 的一个简单方法是:它将每个列表构造函数替换为所提供函数的应用程序。您的第一个示例将转换为:
10 - (11 - 54)
来自:
10 : (11 : [])
我从 Haskell Wikibook 中得到的一条很好的建议可能在这里有用:
通常,您应该在可能是无限的列表或折叠正在构建数据结构的列表上使用
foldr,如果已知列表是有限的并且归结为单个值,则应使用foldl'。foldl(没有打勾)应该很少使用。
【讨论】:
我一直认为http://foldr.com 是一个有趣的插图。请参阅Lambda the Ultimate 帖子。
【讨论】:
1+(1+(1+(1+(1+(1+(1+(1+(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。
如果说foldr 和foldl 中的r ("right") 和l ("left") 指的是右关联 和 left associativity,要么就这样,要么尝试解释 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
【讨论】:
foldr 和无限列表中到底发生了什么。
我认为以简单的方式实现 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
【讨论】:
好的,让我们看看参数:
返回值:
它首先将函数应用于列表中的最后一个元素和空列表结果。然后它用这个结果和前一个元素重新应用函数,依此类推,直到它需要一些当前结果和列表的第一个元素来返回最终结果。
Fold 使用一个接收元素和一些先前折叠结果的函数围绕初始结果“折叠”一个列表。它对每个元素重复此操作。因此,foldr 从列表的末尾或右侧开始执行此操作。
folr f emptyresult [1,2,3,4] 变成
f(1, f(2, f(3, f(4, emptyresult) ) ) ) 。现在只需在评估中加上括号即可。
需要注意的重要一点是,提供的函数 f 必须将其自己的返回值作为其第二个参数处理,这意味着两者必须具有相同的类型。
来源:my post 如果您认为它可能会有所帮助,我会从命令式非curried javascript 的角度来看待它。
【讨论】:
此 wiki 页面中的图片形象化了foldr(以及foldl)的想法:
比如foldr (-) 0 [1,2,3]的结果是2,可以形象化为:
-
/ \
1 -
/ \
2 -
/ \
3 0
即(从下往上):
1 - ( -1 ) = 2
2 - ( 3 )
3 - 0
所以foldr (\x y -> (x+y)/2) 54 [12, 4, 10, 6] 正在通过以下方式计算:
12 `f` (12.0) = 12.0
4 `f` (20.0)
10 `f` (30.0)
6 `f` 54
【讨论】: