2.10 歸納定義的簡單變化
目前為止,我們所認定「良好」的函數定義是這種形式:
f Zero = ...
f (Suc n) = ... f n ...
我們知道這樣定義出的函數是個全函數、對所有輸入都會終止、和歸納法有密切關係...。 以後的幾個章節中,我們將逐步放鬆限制,允許更有彈性的函數定義模式。 我們先從歸納法的一些較簡單的變化開始。
基底狀況的變化 有些函數的值域是「正整數」或「大於 b 的整數」。 定義這些函數時我們可用這樣的模式:
f1 One = e fb b = e
f1 (Suc n) = ... f1 n ... {-"~~,\qquad\qquad"-} fb (Suc n) = ... fb n ...
我們可把 f1 理解為:另外訂了一個資料型別 data NatP = One | Suc NatP,
以 One 為基底狀況,而 f1 是 NatP 之上的全函數。fb 的情況也類似。
與使用 Nat 的函數混用時,我們就得在這兩個型別之間作轉換。這相當於檢查給 f1 的輸入都是大於 1 的整數。實務上為了方便,我們仍用同一個型別實作 Nat 與 NatP, 就如同實務上用 Int 實作 Nat 一樣。
串列的情形也類似。有些函數若能只定義在「不是空的串列」上,其定義會比較合理。
對於這些函數,我們可想像有這麼一個「非空串列」資料型別
data ListP a = [a] | a : ListP a,
只是為了方便,我們用普通串列實作它。
在一些型別系統更強大的語言中、進行更注重證明的應用時,我確實有將 NatP 與 ListP 做成與 Nat 和 List 不同的型別的經驗與需求。
例 2.12
假設 x ↑ y 傳回 x 與 y 中較大的值。
函數 maximumP 傳回一個非空串列中的最大元素:
maximumP :: ListP Int -> Int
maximumP [x] = x
maximumP (x:xs) = x ↑ maximumP xs
Haskell 標準函式庫中另有一個函數 maximum :: List Int -> Int, 但該函數需假設 Int 中有一個相當於 $-\infty$ 的值存在,以便當作 maximum [] 的結果:
maximum [] = -infty
maximum (x:xs) = x ↑ maximum xs
例 2.13
回顧第2.6.4 節提及的「劃分(partition)」:如果 concat yss = xs, 且yss 中的每個串列都不是空的,則 yss 是 xs 的一個劃分。
如果我們限定 xss 為非空串列,計算所有劃分的函數
partsP :: ListP a -> List (ListP (ListP a)) 可以寫得更簡潔:
partsP [x] = [[[x]]]
partsP (x:xs) = concat (map (extend x) (partsP xs))
where extend x (ys:yss) = [[x]:ys:yss, (x:ys):yss]
由於每個劃分一定是非空串列,extend 不需考慮輸入為 [] 的情況。
多個參數的歸納定義
函數 take/drop 的功能與 takeWhile/dropWhile 很類似,
但其定義方式卻有些不同:
take :: Nat -> List a -> List a
take Zero xs = []
take (Suc n) [] = []
take (Suc n) (x:xs) = x : take n xs
drop :: Nat -> List a -> List a
drop Zero xs = xs
drop (Suc n) [] = []
drop (Suc n) (x:xs) = drop n xs
我們可把 take/drop 想成在自然數上歸納定義成的高階函數:
take (Suc n) 的值是一個 List a -> List a 的函數。
定義 take (Suc n) 時,我們假設 take n 已有定義。
唯一的特殊處是我們另分出兩個子情況:串列為 [], 或串列為 x:xs.
根據「使證明的結構符合程式的結構」的原則,如果要做關於
take/drop 的證明,一種可能做法是也依循著它們的結構:
先拆解自然數,在 n := Suc n 的情況中再分析串列的值。
作為例子,我們來驗證第頁提到的這個性質:
定理 2.14
對所有 n 與 xs, take n xs ++ drop n xs = xs.
在 n 上做歸納。在 n := [] 的情況下,等號兩邊都化簡為 [].
在 n:=Suc n 的情況中,我們再細分出兩種情形:
狀況 n := Suc n, xs := []. 顯然等號兩邊都化簡為 [].
狀況 n := Suc n, xs := x:xs:
take (Suc n) (x:xs) ++ drop (Suc n) (x:xs)
= {- take 與 drop 之定義 -}
x : take n xs ++ drop n xs
= {- 歸納假設 -}
x : xs
然而,本章討論的是所有資料結構都是歸納定義、所有函數也都是全函數的世界。 如果我們的世界中有不終止程式存在,上式便不見得成立了。
練習 2.37
請舉一個在允許不終止程式的 Haskell 中,
take n xs ++ drop n xs = xs 不成立的例子。
練習 2.38
對任何串列 xs,head xs : tail xs = xs 都成立嗎?請舉一個反例。
答案
head [] : tail [] /= [].
練習 2.39
證明對自然數 m, n, take m (take (m+n) xs) = take m xs.
答案
針對 m 做歸納。當 m := Zero, 等號兩邊都歸約成 []。
考慮m := Suc m. 此時針對 xs 做分析。
當 xs := [], 等號兩邊亦都歸約成 []。
考慮 xs := x:xs 的情況:
take (Suc m) (take (Suc m + n) (x:xs))
=== {- (+) 與 take 之定義 -}
take (Suc m) (x : take (m + n) xs)
=== {- take 之定義 -}
x : take m (take (m + n) xs)
=== {- 歸納假設 -}
x : take m xs
=== {- take 之定義 -}
take (Suc m) xs
練習 2.40
證明對自然數 m, n, drop m (take (m + n) xs) = take n (drop m xs).
答案
針對 m 做歸納。當 m := Zero, 等號兩邊都歸約成 take n xs。
考慮m := Suc m. 此時針對 xs 做分析。
當 xs := [], 等號兩邊亦都歸約成 []。
考慮 xs := x:xs 的情況:
drop (Suc m) (take (Suc m + n) (x:xs))
=== {- (+) 與 take 之定義 -}
drop (Suc m) (x : take (m + n) xs)
=== {- take 之定義 -}
drop n (take (m + n) xs)
=== {- 歸納假設 -}
take n (drop m xs)
=== {- take 之定義 -}
take n (drop (Suc m) (x:xs))
練習 2.41
證明對自然數 m, n, drop (m + n) xs) = drop n (drop m xs).
答案
針對 m 做歸納。當 m := Zero, 等號兩邊都歸約成 drop n xs。
考慮m := Suc m. 此時針對 xs 做分析。
當 xs := [], 等號兩邊亦都歸約成 []。
考慮 xs := x:xs 的情況:
drop (Suc m + n) (x:xs)
=== {- (+) 與 take 之定義 -}
drop (m + n) xs
=== {- 歸納假設 -}
drop n (drop m xs)
=== {- drop 之定義 -}
drop n (drop (Suc m) (x:xs))
函數 zip 是另一個例子。我們可把 zip xs ys 視為 xs 之上的歸納定義:
zip :: List a -> List b -> List (a × b)
zip [] ys = []
zip (x:xs) [] = []
zip (x:xs) (y:yz) = (x,y) : zip xs ys
練習 2.42
試定義 zipWith :: (a -> b -> c) -> List a -> List b -> List c,
並證明 zipWith f xs ys = map (uncurry f) (zip xs ys).