Haskell的foldr / foldl定义绊倒新手?对于foldl实际函数需要f(默认情况)x而foldr函数需要f x(默认情况)?

时间:2018-03-13 00:55:22

标签: haskell fold

首先,我理解(几乎)折叠功能。鉴于该功能,我可以轻松地解决将要发生的事情以及如何使用它。

问题在于它的实现方式导致了功能定义的细微差别,需要一些时间来理解。更糟糕的是,大多数折叠的示例都有相同类型的列表和默认情况,这没有帮助因为这些可能会有所不同。

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(即默认情况作为第一个参数传递)。

如果我们在两种情况下都将默认情况相同(两者中的第一个参数或第二个参数),那么函数定义是否会相同?这个选择有什么特别的原因吗?

4 个答案:

答案 0 :(得分:4)

为了让事情更清楚,我会在foldl签名中重命名几个类型的变量...

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

...因此,在这两种情况下,a代表列表元素的类型,而b代表折叠结果的类型。

通过扩展递归定义可以看出foldrfoldl之间的主要区别。 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的函数。 (当然,有人可以foldrflip f相同的签名,然后在定义时使用f而不是a,但这样做除了混乱之外什么都不会。

P.S。:这是一个很好的简单折叠示例,其中bfoldr (:) [] -- 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 (/)

评估foldr
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

定义反向