(这个问题比Haskell更为普遍,但这是我要用的语言。)
foldl
和foldr
之间的区别似乎取决于列表是有序的。也就是说,foldl
和foldl
通过将函数应用于起始值和第一个或最后一个元素,然后应用于第一个应用程序的结果和第二个或倒数第二个元素来折叠列表,然后将第二次应用的结果应用于第三个或倒数第二个元素,等等。
但是Haskell的Data.Set和Data.Map库定义了它们自己的foldl
和foldr
版本。例如,对于地图,它们是这些:
foldr :: (a -> b -> b) -> b -> Map k a -> b
foldl :: (b -> a -> b) -> b -> Map k a -> b -- I've swapped `a` and `b`
-- in the second type signature to clarify the correspondence.
地图和集合未排序。我应该期望为集合和映射定义的foldl
和foldr
版本之间的性能差异,还是foldr f
做与foldl (flip f)
完全相同的事情?
答案 0 :(得分:5)
实际上,集合和映射是 。这就是为什么所有set和map函数都对键类型设置Ord
约束的原因。它们是根据元素类型的自然顺序自动排序的。因此,如果您的集合中包含{3, 5, 2, 1, 4}
,则Haskell会按{1, 2, 3, 4, 5}
的顺序查看(出于折叠目的)。
但是让我们暂时忘记它。假设我们处于一个完美的世界,其中的数据确实是无序的。即使这样,foldl
和foldr
之间的差异仍然很明显。假设我的集合如前所述:{3, 5, 2, 1, 4}
,并且我想对其执行一些操作.*
。
foldl (.*) 0 mySet = ((((0 .* 3) .* 5) .* 2) .* 1) .* 4
foldr (.*) 0 mySet = 3 .* (5 .* (2 .* (1 .* (4 .* 0))))
因此,即使操作恰好是关联的,初始元素也以foldl
与foldr
相对放置。实际上,如果使用错误的折叠执行某些操作,则甚至不起作用。考虑toList
,它在Data.Foldable
中定义为可在任何Foldable
对象(包括列表,地图和集合)上工作。一种可能的实现方式是
toList :: Foldable t => t a -> [a]
toList = foldr (:) []
如果我们要尝试进行foldl
definitelyWrong :: Foldable t => t a -> [a]
definitelyWrong = foldl (:) []
然后它甚至不编译。
wrongfold.hs:5:25: error:
• Occurs check: cannot construct the infinite type: a ~ [a]
Expected type: [a] -> [a] -> [a]
Actual type: a -> [a] -> [a]
这是因为折叠的关联方式不同,甚至可以使用带有两个不同类型参数的累加运算,这从两个类型的签名中也很明显
foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
请注意第一个参数不是a -> a -> a
,而是实际上具有两种不同的类型,这表明排序确实很重要。
答案 1 :(得分:2)
地图和集合未排序。我是否应该期望为集合和地图定义的foldl和foldr版本之间的性能差异
如果引用Data.Set
或Data.Map
的源代码,您会发现它们的元素以二进制树的形式组织:
data Map k a = Bin !Size !k a !(Map k a) !(Map k a)
| Tip
data Set a = Bin !Size !a !(Set a) !(Set a)
| Tip
和Set的foldr
:
foldr f z = go z
where
go z' Tip = z'
go z' (Bin _ x l r) = go (f x (go z' r)) l
使用深度优先搜索按顺序右,当前,左遍历树,因此当foldr (+) 0
适用于跟随树时:
1
/ \
4 2
\
3
给予
4 + (1 + (2 + (3 + 0)))
foldl f z = go z
where
go z' Tip = z'
go z' (Bin _ x l r) = go (f (go z' l) x) r
按顺序左,当前,右,将foldl (+) 0
应用于上面的树,给出:
((((0 + 4) + 1) + 2) + 3)
它表明Set的foldr
和foldl
等同于以下列表:
foldr (+) 0 [4, 1, 2, 3] = 4 + (1 + (2 + (3 + 0)))
foldl (+) 0 [4, 1, 2, 3] = ((((0 + 4) + 1) + 2) + 3)
与Data.Map
类似的情况,在此不再赘述。
此外,我们知道,foldr
可以应用于无限列表(但foldl
不能),例如:
take 10 $ foldr ((:) . sum) [] $ chunksOf 3 [1..] = [6,15,24,33,42,51,60,69,78,87]
(这里chunksOf
将列表分组为[[1,2,3], [4,5,6]...]
)
但是树何时有路径怎么办呢?
1
/ \
4 2
\
3
\
... <- infinite path
Set的foldr
是否像上面提到的那样充当列表? (我想答案是肯定的,您可以自己检查一下)
文件夹f是否与文件夹(翻转f)完全相同?
否,如上图所示,作为源代码:
foldr = ... go (f x (go z' r)) l
和
foldl (flip f) = ... go (f x (go z' l)) r
树的遍历顺序不同,但是foldr
和foldl
之间的一般关系可以在这篇文章中找到:Defining foldl in terms of foldr