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'
,因爲後者比前者高效。