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

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