6 摺

6.3 左摺、串列同構、與 Paramorphism

為了完整性,本節介紹與串列的摺相關的另外幾個組件函數。 初次閱讀時可跳過本節。

6.3.1 左摺

除了 foldr, Haskell 的標準函式庫有另一個函數 foldl, 名字中的字母 l 為「左」之意: foldr 將串列中的元素往右括,foldl 則往左括,例如,

 foldl rhd e [x0,x1,x2,x3] = (((e `rhd` x0) `rhd` x1) `rhd` x2) `rhd` x3 

有了 foldl, 串列反轉可直接定義為 reverse = foldl (\xs x -> x:xs) [].

函數 foldl 可在輸入串列的長度上歸納定義如下:

foldl :: (b -> a -> b) -> b -> List a -> b
foldl rhd e []         = e
foldl rhd e (xs++[z])  = foldl rhd e xs `rhd` z

由於運算元 rhd 右邊的參數才是目前的元素,其型別為 b -> a -> b. 由此出發,我們不難導出下述將串列從左邊開始拆解的定義:

foldl rhd e []      = e
foldl rhd e (x:xs)  = foldl rhd (e `rhd` x) xs 

我們可注意到這是一個尾遞迴定義, 因此,如果將 sum, prod 等函數定義為 foldl (+) 0, foldl (*) 1 等等,執行時可不佔用堆疊的空間。 由於這個特性,對一些將 Haskell 用於著重效率的應用的人們來說,foldl 才是他們較常使用的「摺」。 為了效率因素更常被使用的可能是另一個嚴格(strict)版的函數 foldl', 該函數在遞迴呼叫前會先將 e `rhd` x 規約成正規式,避免尚待計算的算式被累積著。

關於 foldl 與 foldr 的關係,Bird [1998] 提及了三個定理。首先,

定理 6.14 第一對偶定理(The First Duality Theorem)

如果 oplus :: a -> a -> a 滿足遞移律,以 e :: a 為單位元素,則 foldr oplus e xs = foldl oplus e xs.

提醒一下:該定理及本節的其他定理中,xs 需為歸納定義、有限長度的串列。 遞移律意指 x ⊕ (y ⊕ z) = (x ⊕ y) ⊕ z --- 由於此項要求,oplus 的型別得是 a -> a -> a. 由於 (+), (*), (↓), (↑) 等運算元都有遞移律,sum, prod, maixmum, minimum 等函數都可用 foldl 定義。事實上,他們在標準 Haskell Report 中的定義便是如此。

定理6.14 其實是下述定理的特例。 函數 foldl 有個尾遞迴定義,其中參數 e 的角色就像是個累積參數。 第5.6.3 節中曾提及,尾遞迴函數時常和遞移律有關。 函數 foldl 是否也牽涉了某種遞移律呢?

定理 6.15 第二對偶定理(The Second Duality Theorem)

如果二元運算元 lhd :: a -> b -> b 及 rhd :: b -> a -> b 滿足 x `lhd` (y `rhd` z) = (x `lhd` y) `rhd` z 以及 x `lhd` e = e `rhd` x, 則 foldr lhd e xs = foldl rhd e xs.

定理 6.15 中的 x `lhd` (y `rhd` z) = (x `lhd` y) `rhd` z 可視為一種擴充到兩個運算元的遞移律。定理 6.14 則是 lhd = rhd 的特殊情況。

定理 6.15 的證明頗值得研究:

證明

我們在 xs 上做歸納。基底狀況 xs := [] 中,等號兩邊都歸約為 e. 至於 xs := x : xs 的情況,我們把兩邊都化簡並使用歸納假設,推論如下:

      foldr lhd e (x:xs)
 ===    {- foldr 之定義 -}
      x `lhd` foldr lhd e xs
 ===    {- 歸納假設 -}
      x `lhd` foldl rhd e xs
 ===    {- 論證如後述 -}
      foldl rhd (x `lhd` e) xs
 ===    {- 假設: x `lhd` e = e `rhd` x -}
      foldl rhd (e `rhd` x) xs
 ===    {- foldl 之定義 -}
      foldl rhd e (x:xs) 

在等式推論中段,我們希望 x `lhd` foldl rhd e xs = foldl rhd (e `rhd` x) xs. 這會需要另一個歸納證明。 但我們若直接證明此等式,到中途便會無法進行,且會發現我們需要稍做一下推廣,改證明一個較強的性質:

  x `lhd` foldl rhd y xs = foldl rhd (x `lhd` y) xs     (6.1 )

兩者的差別是後者不針對特定的值 e, 而是任何的 y. 這是一個「較強的性質反而比較好證明」的例子。

等式([RefUndefined])的證明如此進行:在 xs := [] 的情況中,等號兩側都規約為 x `lhd` y. 歸納情況 xs := z : xs 證明如下:

      x `lhd` foldl rhd y (z:xs)
 ===    {- foldl 之定義 -}
      x `lhd` foldl rhd (y `rhd` z) xs
 ===    {- 歸納假設 -}
      foldl rhd (x `lhd` (y `rhd` z)) xs
 ===    {- 假設:x `lhd` (y `rhd` z) = (x `lhd` y) `rhd` z -}
      foldl rhd ((x `lhd` y) `rhd` z) xs
 ===    {- foldl 之定義 -}
      foldl rhd (x `lhd` y) (z:xs) 

最後一個對偶定理則確認了我們的一個直覺理解:將一個串列反轉做 foldl, 便相當於 foldr:

定理 6.16 第三對偶定理(The Third Duality Theorem)

對所有 f :: a -> b -> b, e :: b, 以及 xs :: List a, foldr f e xs = foldl (flip f) e (reverse xs). 其中 flip f x y = f y x.

6.3.2 串列同構

考慮函數 h :: List a -> b. 如果存在 e :: b, f :: a -> b, 和 odot :: b -> b -> b 使得 h 滿足下列等式:

h []          = e
h [x]         = f x
h (xs ++ ys)  = h xs ⊙ h ys 

我們便說 h 是一個串列同構(list homomorphism), 記為 h = hom odot f e. 注意第三個等式蘊含 odot(至少在 h 的值域內)須滿足結合律: h xs ⊙ (h ys ⊙ h zs) = h (xs++(ys++zs)) = h ((xs++ys)++zs) = (h xs ⊙ h ys) ⊙ h zs.

串列同構有平行計算的可能性:計算 hom odot f e xs 時,如果 xs 有不只一個元素,我們可將 xs 由中間任意截成兩段,分別給不同的處理器計算,再將結果用 odot 結合。

顯然,hom dot f e 可以寫成 foldr, 也能寫成 foldl:

  hom odot f e  === foldr (\x y -> f x ⊙ y) e
                === foldl (\y x -> y ⊙ f x) e 

下述定理則告訴我們反過來也成立:如果 h 同時可寫成 foldr 及 foldl, 則 h 是一個串列同構:

定理 6.17 第三同構定理(The Third Homomorphism Theorem)

考慮 h :: List a -> b。如果存在 e :: b, lhd :: a -> b -> b, 及 rhd :: b -> a -> b 使得 h = foldr lhd e = foldl rhd e, 則存在 odot :: b -> b -> b 使得 h = hom odot f e (其中 f x = x `lhd` e = e `rhd` x).

6.3.3 Paramorphism 與本原遞迴

如果我們把 foldr 的限制放寬,允許 xs 出現在遞迴呼叫之外,得到的模式稱為 paramorphism. 串列版的 paramorphism 可定義如下:

para :: (a -> List a -> b -> b) -> b -> List a -> b
para f e []      = e
para f e (x:xs)  = f x xs (para f e xs) 
para f e = fst . foldr (\x (y, xs) -> (f x xs y, x:xs)) (e, [])