Haskell:用foldr定义foldl

Haskell:用foldr定义foldl

基础知识

fold操作是把一个列表聚合成一个值的过程,而在此基础上有foldlfoldr两种对称的实现。两个函数的一种定义如下:

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 atid的类型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的类型是ay的类型是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',因为后者比前者高效。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章