Haskell:用foldr定义foldl
基础知识
fold操作是把一个列表聚合成一个值的过程,而在此基础上有foldl
和foldr
两种对称的实现。两个函数的一种定义如下:
foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
foldl f _ [] = _
foldl f z (x:xs) = foldl f (f z x) xs
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
foldr f _ [] = _
foldr f z (x:xs) = f x (foldr f z xs)
两者的区别是,foldl
是对列表中的元素,从左到右地执行运算,而foldr
是从右向左进行操作。
Prelude> foldr (-) 0 [1,2,3]
2
# 1-(2-(3-0))=2
Prelude> foldl (-) 0 [1,2,3]
-6
# ((0-1)-2)-3=-6
Prelude> foldr (++) "aa" ["bb","11","22"]
"bb1122aa"
# "bb"+("11"+("22"+"aa"))="bb1122aa"
Prelude> foldl (++) "aa" ["bb","11","22"]
"aabb1122"
# (("aa"+"bb")+"11")+"22"="aabb1122"
foldl f z xs
的执行逻辑是f . (f (f z x1) x2).. xn
,foldr f z xs
的执行逻辑是f x1 (f x2 (..(f xn z) .. )
,所以前者是从前往后,后者是从后往前,而且两者的f
具有不同的函数类型。
事实上,用foldr
可以实现foldl
:
foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
foldl f z xs = foldr (\x g y -> g (f y x)) id xs z
在上面的定义中,y :: b
,类型和z
一致。
要一眼看出来这个定义的正确性确实是比较困难的。下面是一些说明。
类型推导
这一节,我们试图明确定义中每个符号的类型,从而对这个定义有更好的理解。
1.特地在这写一下f
的类型:
f :: b -> a -> b
f z x = z0 :: b
2.到(\x g y -> g (f y x))
之前
对于在定义式右侧出现的foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
,这里进行两次alpha替换,a
替换为at
, b
替换为bt
.
foldr (\x g y -> g (f y x)) id xs z
的运算顺序是先计算foldr (\x g y -> g (f y x)) id xs
,再传入参数z
.所以foldr (\x g y -> g (f y x)) id xs :: b -> b
,所以bt = b -> b
.
右侧根据定义,xs
的类型Foldable t => t at
,id
的类型bt
,和左侧对比知道at = a
.(注:id :: a -> a, id x = x
.)
代入at, bt
得到(\x g y -> g (f y x))
的类型a -> (b -> b) -> (b -> b)
.
3.(\x g y -> g (f y x))
内部
我们一直尽量保证符号的一致性,所以这里x
的类型是a
,y
的类型是b
(和z
一样)。按照模式匹配的特点,知道g
接收一个类型为b
的单参数,组合之后返回一个b -> b
的值,所以g
的类型为b -> (b -> b)
.做完了!
总结:
1.系统对运算的反转,这个从定义式里面就看得到,lambda输入中的先x后y变成了调用时的先y后x.
2.foldr
的三个输入都是函数,相当于自始至终都在处理函数,为我们提供了一个很好的使用高阶函数,从函数层面思考问题的示例。
恒等证明
这一节,我们证明定义的正确性,并通过推导说明各个变量的作用。
foldl f z xs = foldr (\x g y -> g (f y x)) id xs z
= ((\x g y -> g (f y x)) x1 (foldr (\x g y -> g (f y x)) id [x2 .. xn])) z
= ((\g y -> g (f y x1)) (foldr (\x g y -> g (f y x)) id [x2 .. xn])) z
= (g1 (foldr (\x g y -> g (f y x)) id [x2 .. xn])) z
= (g1 (g2 (foldr (\x g y -> g (f y x)) id [x3 .. xn]))) z
= ..
= (g1 (g2 (.. (gn (foldr (\x g y -> g (f y x)) id [])) .. ))) z
= (g1 (g2 (.. (gn id) .. ))) z
= g1 (g2 (.. (gn id) .. )) z
= (g2 (.. (gn id) .. )) (f z x1)
= g2 (g3 (.. (gn id) .. )) (f z x1)
= (g3 (.. (gn id) .. )) (f (f z x1) x2)
= ..
= f (.. (f (f z x1) x2) .. ) xn
in which xs = [x1 .. xn], gi = \g y -> g (f y xi)
最终结果和我们期待的结果相同。(我就不按照foldl
的原定义再推一遍了,容易见得最后也是推到这个结果。)这说明这个定义是对的。
总结(续):
3.lambda运算不但用于换序,还把展开过程中函数的嵌套表示了出来。也就是说,最后的嵌套结果是这个lambda运算实现的。其他的x, y
和我们的预期类似,分别是列表元素和累加结果的中间状态。
4.换序的实质是把从尾到头结合的东西从累加变量z
变成了函数id
,而运算从迭代更新值变成了迭代更新函数。因为函数结合调用的顺序是和迭代顺序相反的,所以越晚结合在迭代式的函数反而越早被执行(越早因为求值而拆解下来),从而实现了换序。
如果上面的过程不好读,可以试着自己人肉执行一下foldl (-) 1 [2, 4, 8]
.
Q&A
Q: foldl
能否模拟出foldr
?
A: 不行,原理性问题:前者不能处理无穷列表,后者可以(通过惰性求值)。
(持续更新,想到新问题就补)
总结
高阶函数的特性就是函数既可以做输入参数,也可以做输出参数。此处的对高阶函数的foldr
的使用非常灵活,是一个值得学习的案例。
参考和扩展阅读
Haskell – 用foldr表示foldl
这篇文章的内容和本文很像,可以作为本文的补充。
Haskell foldl, foldr, foldl’
这篇文章说明,能用foldr
就不要用foldl
,以及非得使用foldl
时应该使用foldl'
,因为后者比前者高效。