2 歸納定義與證明

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).