首先,我理解(几乎)折叠功能。鉴于该功能,我可以轻松地解决将要发生的事情以及如何使用它。
问题在于它的实现方式导致了功能定义的细微差别,需要一些时间来理解。更糟糕的是,大多数折叠的示例都有相同类型的列表和默认情况,这没有帮助因为这些可能会有所不同。
Usage:
foldr f a xs
foldl f a xs
where a is the default case
definition:
foldr: (a -> b -> b) -> b -> [a] -> b
foldl: (a -> b -> a) -> a -> [b] -> a
在定义中我理解a
是要传递的第一个变量,b
要传递给函数的第二个变量。
最终我理解这种情况正在发生,因为当f
最终在foldr
中进行评估时,它被实现为f x a
(即默认情况作为第二个参数传递)。但对于foldl
,它实现为f a x
(即默认情况作为第一个参数传递)。
如果我们在两种情况下都将默认情况相同(两者中的第一个参数或第二个参数),那么函数定义是否会相同?这个选择有什么特别的原因吗?
答案 0 :(得分:4)
为了让事情更清楚,我会在foldl
签名中重命名几个类型的变量...
foldr: (a -> b -> b) -> b -> [a] -> b
foldl: (b -> a -> b) -> b -> [a] -> b
...因此,在这两种情况下,a
代表列表元素的类型,而b
代表折叠结果的类型。
通过扩展递归定义可以看出foldr
和foldl
之间的主要区别。 f
foldr
中的foldr f a [x,y,z] = x `f` (y `f` (z `f` a))
申请与权利相关联,初始值显示在元素的右侧:
foldl
使用foldl f a [x,y,z] = ((a `f` x) `f` y) `f` z
,反之亦然:关联位于左侧,初始值显示在左侧(正如Silvio Mayolo在his answer强调的那样)它必须如何使初始值在最里面的子表达式中:)
foldr
这就解释了为什么list元素是赋给foldl
的函数的第一个参数,第二个参数是给foldl
的函数。 (当然,有人可以foldr
给flip f
相同的签名,然后在定义时使用f
而不是a
,但这样做除了混乱之外什么都不会。
P.S。:这是一个很好的简单折叠示例,其中b
和foldr (:) [] -- id
foldl (flip (:)) [] -- reverse
类型彼此不同:
populateMatrixFromFile
答案 1 :(得分:1)
折叠是一种变形,或者是一种“摧毁”的方式。将数据结构转换为标量。在我们的案例中,我们"拆除"一个列表。现在,在使用catamorphism时,我们需要为每个数据构造函数都有一个案例。 Haskell列表有两个数据构造函数。
[]
也就是说,(:)
是一个构造函数,它不带参数并产生一个列表(空列表)。 foldr
是一个构造函数,它接受两个参数并创建一个列表,将第一个参数添加到第二个参数上。所以我们需要有两个案例。 foldr :: (a -> b -> b) -> b -> [a] -> b
是catamorphism的直接例子。
(:)
如果遇到(:)
构造函数,将调用第一个函数。它将传递第一个元素(第一个参数foldr
)和递归调用的结果(在(:)
的第二个参数上调用[]
)。第二个参数,"默认情况"正如你所说的那样,当我们遇到foldr (+) 4 [1, 2, 3]
1 + (2 + (3 + 4))
构造函数时,在这种情况下我们只使用默认值本身。所以最终看起来像这样
foldl
现在,我们可以用同样的方式设计foldl
吗?当然。 foldr
并非(确切地说)是一种变形现象,但它在精神上表现得像一样。在foldl
中,默认情况是最里面的值;它仅用于"最后一步"当我们用完列表元素时,递归在foldl (+) 4 [1, 2, 3]
((4 + 1) + 2) + 3
中,我们为了一致性做同样的事情。
foldl
让我们更详细地说明这一点。 foldl (+) 4 [1, 2, 3]
foldl (+) (4 + 1) [2, 3]
foldl (+) ((4 + 1) + 2) [3]
foldl (+) (((4 + 1) + 2) + 3) []
-- Here, we've run out of elements, so we use the "default" value.
((4 + 1) + 2) + 3
可以被认为是使用累加器来有效地获得答案。
public class BigIntegerEvaluator implements TypeEvaluator<BigInteger>{
@Override
public BigInteger evaluate(float v, BigInteger start, BigInteger end) {
//todo: implement this
return start + v * (end - start);
}
}
所以我认为对你的问题的简短回答是,它在数学上更加一致(并且更有用),以确保基本案例始终处于递归调用的最内部位置,而不是聚焦它一直在左边或右边。
答案 2 :(得分:1)
考虑调用foldl (+) 0 [1,2,3,4]
和foldr (+) 0 [1,2,3,4]
并尝试可视化他们的行为:
foldl (+) 0 [1,2,3,4] = ((((0 + 1) + 2) + 3) + 4)
foldr (+) 0 [1,2,3,4] = (0 + (1 + (2 + (3 + 4))))
现在,让我们尝试在每一步中将参数交换为(+)
的调用:
foldl (+) 0 [1,2,3,4] = (4 + (3 + (2 + (1 + 0))))
请注意,尽管存在对称性,但这与之前的foldr
不同。我们仍在从列表左侧积累,我只是改变了操作数的顺序。
在这种情况下,因为加法是可交换的,我们得到相同的结果,但是如果你试图折叠一些非交换函数,例如字符串连接,结果不同。折叠["foo", "bar", "baz"]
,您将获得"foobarbaz"
或"bazbarfoo"
(而foldr
也会产生"foobarbaz"
,因为字符串连接是关联的。)
换句话说,这两个定义使得两个函数对于可交换和关联二进制运算(如常见的算术加法/乘法)具有相同的结果。将参数交换到累积函数会破坏这种对称性并迫使您使用flip
来恢复对称行为。
答案 3 :(得分:1)
使用前缀表示法(+)右侧折叠
foldr (+) 10 [1,2,3]
=> + 1 (+ 2 (+ 3 10))
=> + 1 (+ 2 13)
=> + 1 15
=> 16
foldl (+) 10 [1,2,3]
=> + (+ (+ 10 1) 2) 3
=> + (+ 11 2) 3
=> + 13 3
=> 16
两个折叠都评估相同的结果,因为(+)
是可交换的,即
+ a b == + b a
让我们看看当函数不可交换时会发生什么,例如除法或指数
foldl (/) 1 [1, 2, 3]
=> / (/ (/ 1 1) 2) 3
=> / (/ 1 2) 3
=> / 0.5 3
=> 0.16666667
foldr (/) 1 [1, 2, 3]
=> / 1 (/ 2 (/ 3 1))
=> / 1 (/ 2 3)
=> / 1 0.666666667
=> 1.5
现在,让我们用函数flip (/)
let f = flip (/)
foldr f 1 [1, 2, 3]
=> f 1 (f 2 (f 3 1))
=> f 1 (f 2 0.3333333)
=> f 1 0.16666667
=> 0.16666667
同样,让我们使用foldl
f
foldl f 1 [1, 2, 3]
=> f (f (f 1 1) 2) 3
=> f (f 1 2) 3
=> f 2 3
=> 1.5
因此,在这种情况下,翻转折叠函数的参数的顺序可以使左折叠返回与右折叠相同的值,反之亦然。但这并不能保证。例如:
foldr (^) 1 [1, 2, 3] = 1
foldl (^) 1 [1, 2, 3] = 1
foldr (flip (^)) 1 [1,2,3] = 1
foldl (flip (^)) 1 [1,2,3] = 9 -- this is the odd case
foldl (flip (^)) 1 $ reverse [1,2,3] = 1
-- we again get 1 when we reverse this list
顺便说一下,reverse
相当于
foldl (flip (:)) []
但尝试使用foldr