2.6 更多歸納定義與證明
為讓讀者熟悉,本節中我們多看一些自然數或串列上的歸納定義。
2.6.1 filter, takeWhile, 與 dropWhile
filter 我們曾見過的函數 filter 可寫成如下的歸納定義:
filter :: (a -> Bool) -> List a -> List a
filter p [] = []
filter p (x:xs) = if p x then x : filter p xs else filter p xs
在 filter 的許多性質中,我們試著證明下述性質作為例子:
定理 2.4
filter p . map f = map f . filter (p . f).
和定理2.2 一樣,待證式的左右邊都是函數。
根據外延相等,我們將左右邊各補上參數 xs,並在 xs 上做歸納:
filter p (map f xs) = map f (filter (p . f) xs) 情況 x := [] 中左右邊都可化簡成[].
我們看看 xs := x:xs 的情況:
filter p (map f (x:xs))
=== {- map 之定義 -}
filter p (f x : map f xs)
=== {- filter 之定義 -}
if p (f x) then f x : filter p (map f xs) else filter p (map f xs)
=== {- 歸納假設 -}
if p (f x) then f x : map f (filter (p . f) xs) else map f (filter (p . f) xs)
=== {- map 之定義 -}
if p (f x) then map f (x : filter (p . f) xs) else map f (filter (p . f) xs)
=== {- 因 f (if p then e1 else e2) = if p then f e1 else f e2, 如後述 -}
map f (if p (f x) then x : filter (p . f) xs else filter (p . f) xs)
=== {- filter 之定義 -}
map f (filter (p . f) (x:xs))
終止與證明
上述證明的倒數第二步為將 map f 提到外面,用了一個關於 if 的性質:
f (if p then e1 else e2) = if p then f e1 else f e2 (2.4 )
這性質對嗎?若 p 成立,左右手邊都化簡為f e1, 若 p 不成立,左右手邊都化簡為 f e2. 因此 (2.4 ) 應該成立,是嗎?
答案是:如果我們假設的世界中有不終止的程式,(2.4 )便不正確了。
例如,當 f 是 three x = 3,而 p 是個永遠執行、不終止的算式(例:let b = not b in b):
three (if p then e1 else e2) {-"~\stackrel{?}{=}~"-} if p then three e1 else three e2 (2.5 )
上述式子的左手邊直接化簡成3, 但右手邊卻不會終止,因為 if 得知道 p 的值。
我們找到了 (2.4 ) 的反例!
在允許可能不終止的程式存在的世界中,(2.4 )得多些附加條件。
通常的做法是限定 f 須是個嚴格函數, 意即 f 的輸入若不終止,f也不會終止。
但(2.4 )並不是唯一帶著附加條件的性質 --- 許多常用性質都得加上類似的附加條件。
所有狀況分析也都得將不終止考慮進去,例如,自然數除了 Zero 與 Suc n 之外,還多了第三種情況「不終止」。Bird [1987] 就採用這種作法。
推論與證明變得更複雜。
有些人因此較喜歡另一條路:藉由種種方法確保我們只寫出會終止的程式,便可假設我們確實活在所有程式都正常終止的世界中。
保護式 v.s. 條件分支 有些人喜歡用保護式語法定義 filter:
filter p [] = []
filter p (x:xs) | p x = x : filter p xs
| otherwise = filter p xs
若在此定義下證明定理2.4 ,依「證明的結構與程式的結構相同」的原則,順理成章地,我們可在 xs:=x:xs 中再分出 p (f x) 成立與不成立的兩個子狀況:
狀況
xs := []: ...狀況
xs := x:xs: ...狀況
p (f x):filter p (map f (x:xs)) ={- f x : filter p (map f xs) = ...p (f x)成立 -}狀況
not (p (f x)):filter p (map f (x:xs)) ={- filter p (map f xs) = ...(not p (f x))-}
這個定義中不用 if, 因此證明中也用不上 (2.4 ), 但該證明要成立仍須假設所有程式都正常終止 --- 我們少證了一個 「p (f x) 不終止」的情況(而確實,在此情況下(2.4 )並不成立)。喜歡用哪個方式純屬個人偏好。
前幾章提過的 takeWhile 與 dropWhile 兩函數型別與 filter 相同。他們可寫成如下的歸納定義:
takeWhile :: (a -> Bool) -> List a -> List a
takeWhile p [] = []
takeWhile p (x:xs) = if p x then x : takeWhile p xs else []
dropWhile :: (a -> Bool) -> List a -> List a
dropWhile p [] = []
dropWhile p (x:xs) = if p x then dropWhile p xs else x:xs
兩者都是在輸入串列上做歸納。兩者也都可用保護式語法定義。
練習 2.14
證明 takeWhile p xs ++ dropWhile p xs = xs.
練習 2.15
以保護式語法定義 takeWhile 與 dropWhile, 以此定義做做看習題 2.14 .
2.6.2 elem 與不等式證明
不等式證明 給定如下的定義,elem x xs 判斷 x 是否出現在串列 xs 中:
elem x [] = False
elem x (y:xs) = (x == y) || elem x xs
目前為止,我們所練習的都是以(=)將式子串起的等式證明。
以下以elem為例,我們嘗試證明一個「不等式」:
elem z xs ==> elem z (xs ++ ys)
以口語說出的話:「若z 出現在xs 中,z 也出現在xs ++ ys 中」。
欲證明上式,該從哪一側推到哪一側呢?
一般認為從式子較長、或結構較複雜的那側開始,化簡成較短、較簡單的那側,是較容易的。
因此我們嘗試由右側推到左側:由 elem z (xs ++ ys) 開始,尋找使之成立的條件,
並希望 elem z xs 是足夠的。
在 xs 上做歸納。基底 xs := [] 的狀況在此省略,
看 xs:=x:xs 的狀況:
elem z ((x:xs) ++ ys)
<=> {- (++) 之定義 -}
elem z (x : (xs ++ ys))
<=> {- elem 之定義 -}
(z==x) || elem z (xs ++ ys)
<== {- 歸納假設 -}
(z==x) || elem z xs
<=> {- elem 之定義 -}
elem z (x:xs)
讀者可注意:第1, 2, 4 步使用的邏輯關係都是 (<=>) ,第 3 步卻是 (<==),因此整個證明建立了「若elem z (x:xs),則elem z ((x:xs) ++ ys)」。
練習 2.16
證明 not (elem z (xs ++ ys)) ==> not (elem z xs).
練習 2.17
證明 elem z xs ==> elem z (ys ++ xs).
練習 2.18
證明(∀ x . p x ==> q x) ==> all p xs ==> all q xs.
其中 all 的定義為:
all :: (a -> Bool) -> List a -> Bool
all p [] = True
all p (x:xs) = p x ⋀ all p xs
練習 2.19
證明 all (`elem` xs) (filter p xs). 其中 x `elem` xs 是 elem x xs 的中序寫法。 我們可能需要習題 2.17 和 2.18 的結果,以及下述性質:
if p then x else x = x (2.6 )
答案
在 xs 上做歸納。
all (`elem` (x:xs)) (filter p (x:xs))
<=> all (`elem` (x:xs)) (if p x then x:filter p xs else filter p xs)
<=> if p x then all (`elem` (x:xs)) (x:filter p xs)
else all (`elem` (x:xs)) (filter p xs)
<=> if p x then x `elem` (x:xs) ⋀ all (`elem` (x:xs)) (filter p xs)
else all (`elem` (x:xs)) (filter p xs)
<== if p x then all (`elem` (x:xs)) (filter p xs)
else all (`elem` (x:xs)) (filter p xs)
<=> {- \eqref{eq:ifpxx} -}
all (`elem` (x:xs)) (filter p xs)
<== {- 因習題 2.17, z `elem` (x:xs) <== z `elem` xs -}
all (`elem` xs) (filter p xs)
<== True
2.6.3 串列區段
前段與後段
本章目前為止討論的歸納定義都依循著這樣的模式:欲定義 f :: List a -> b, 只要為 f [] 與 f (x:xs) 找到定義。在定義後者時,只需定義出由 f xs 做出 f (x:xs) 的關鍵一步。
目前為止,這關鍵一步都是加一、加上一個元素等簡單的動作。現在我們來看些更複雜的例子。
例1.19 中曾提及:如果一個串列 xs 可分解為 ys ++ zs, 我們說 ys 是 xs 的一個前段(prefix),
zs 是 xs 的一個後段(suffix).
例如,串列 [1,2,3] 的前段包括 [], [1], [1,2], 與[1,2,3] (注意:[]是一個前段,串列 [1,2,3] 本身也是), 後段則包括 [1,2,3], [2,3], [3], 與 []。我們是否能定義一個函數 inits :: List a -> List (List a), 計算給定串列的所有前段呢?
例1.19 給的答案是:
inits xs = map (\n -> take n xs) [0 .. length xs]
如果不用組件,改用歸納定義呢?我們試試看:
inits :: List a -> List (List a)
inits [] = ?
inits (x:xs) = ?
基底狀況 inits [] 的可能選擇是 [[]] (見後述)。至於歸納步驟該怎麼寫?
我們用例子來思考。比較 inits [2,3] 與 inits [1,2,3]:
inits [2,3] = [[],[2],[2,3]]
inits [1,2,3] = [[],[1],[1,2],[1,2,3]]
假設我們已算出 inits [2,3], 如何把它加工變成 inits [1,2,3]? 請讀者暫停一下,思考看看!
一個思路是:如果在 [[],[2],[2,3]] 中的每個串列前面都補一個 1, 我們就有了 [[1],[1,2],[1,2,3]]. 再和 inits [1,2,3] 比較,就只差一個空串列了!
因此 inits 的一種定義方式是:
inits :: List a -> List (List a)
inits [] = [[]]
inits (x:xs) = [] : map (x:) (inits xs)
在此得提醒:有些讀者認為基底狀況 inits [] 的值選為 [[]],是因為結果的型別是 List (List a)
(直覺地把每個 List 都對應到一組中括號,或認為 [[]] 是型別為 List (List a) 的最簡單的值)。
但事實並非如此:畢竟,[] 的型別也可以是 List (List a)!
我們讓 inits [] = [[]] 的原因是空串列 [] 的「所有前段」只有一個,恰巧也是 []。
就如同在自然數上的歸納函數定義中,有些基底狀況是 0, 有些是 1, 有些是別的值,
此處我們也依我們的意圖,選定最合適的基底值。
練習 2.20
試定義 initsp :: List a -> List (List a), 計算一個串列的所有非空前段。例如 initsp [1,2,3] 是 [[1],[1,2],[1,2,3]]。當然,其中一個定義方式是 initsp = tail . inits. 你能以歸納方式定義出 initsp 嗎?
initsp [] = ?
initsp (x:xs) = ?
答案
initsp :: List a -> List (List a)
initsp [] = []
initsp (x:xs) = [x] : map (x:) (initsp xs)
練習 2.21
我們驗證一下 inits 在例1.19 中的組件定義與本章的歸納定義是相等的。定義 upto :: Nat -> List Nat:
upto Zero = [Zero]
upto (Suc n) = 0 : map (Suc) (upto n)
使得 upto n = [0.. n].
假設 inits 已如本節一般地歸納定義,證明對所有 xs,
inits xs = map (\n -> take n xs) (upto (length xs)).
您可能會需要 map 融合定理(定理2.2 ),
答案
在 xs 上做歸納。
情況 xs := []. 此時等號兩邊都是 [[]].
情況 xs := x:xs.
map (\n -> take n (x:xs)) (upto (length (x:xs)))
=== {- length 之定義 -}
map (\n -> take n (x:xs)) (upto (Suc (length xs)))
=== {- upto 之定義 -}
map (\n -> take n (x:xs)) (0 : map (Suc) (upto (length xs)))
=== {- length 之定義 -}
map (\n -> take n (x:xs)) (0 : map (Suc) (upto (length xs)))
=== {- map之定義,take 0 (x:xs) = [] -}
[] : map (\n -> take n (x:xs)) (map (Suc) (upto (length xs)))
=== {- 定理2.2:map f . map g = map (f.g) -}
[] : map (\n -> take (Suc n) (x:xs)) (upto (length xs))
=== {- take 之定義 -}
[] : map (\n -> x : take n xs) (upto (length xs))
=== {- 定理2.2:map f . map g = map (f.g) -}
[] : map (x:) (map (\n -> take n xs) (upto (length xs)))
=== {- 歸納假設 -}
[] : map (x:) (inits xs)
=== {- inits 之定義 -}
inits (x:xs)
定義傳回後段的函數 tails 時可依循類似的想法:
如何把 tails [2,3] = [[2,3],[3],[]]加工,得到 tails [1,2,3] = [[1,2,3],[2,3],[3],[]]?
這次較簡單:加上 [1,2,3] 即可。
的確,串列 x:xs 的後段包括 x:xs 自己,以及 xs 的後段:
tails :: List a -> List (List a)
tails [] = [[]]
tails (x:xs) = (x:xs) : tails xs
在習題 2.45 中我們將證明一個將 inits 與 tails 牽上關係的定理:將 inits 傳回的前段與 tails 傳回的後段照其順序對應,每對接起來都是原來的串列。
連續區段
給定一個串列,許多傳統最佳化問題的目標是計算符合某條件的連續區段(簡稱「區段」)。
例如,[1,2,3] 的區段包括[],[1],[2],[3],[1,2],[2,3], 以及
[1,2,3]本身。
我們可用 inits 與 tails 得到一個串列的所有區段。
segments :: List a -> List (List a)
segments = concat . map inits . tails
但 segments 無法寫成本章目前這種形式的歸納定義。我們將在以後的章節再討論到 segments.
練習 2.22
試著把 segments 寫成如下的歸納定義:
segments :: List a -> List (List a)
segments [] = ?
segments (x:xs) = ... segments xs ...
在歸納步驟中希望由 segments xs 湊出 segments (x:xs)。
這是能在不對輸入串列(型別為List a)做任何限制之下做得到的嗎?
如果做不到,為什麼?
答案
考慮如何從 segments [1,2,3] 湊出 segments [0,1,2,3].
前者應當有的結果可能是:
[[],[1],[1,2],[1,2,3],[2],[2,3],[3]]
而 segments [0,1,2,3] 的結果可能是:
[[],[0],[0,1],[0,1,2],[0,1,2,3],[1],[1,2],[1,2,3],[2],[2,3],[3]]
乍看之下,要多做的一步是:在空串列或所有1開頭的串列前面補上 0.
之所以只擴充1開頭的串列,因為只有這些是原本便靠在左邊,和 0 相鄰的
(也就是說它們是[1,2,3]的前段)。
然而,當輸入串列型別為 List a(而不是特定的 List Int, List Char 時),我們無法用比較的方式找出這些前段。函數 segments 傳回的資訊不夠多。如果要歸納定義,我們必須把「前段」們和其他的區段分開。
2.6.4 插入、排列、子串列、與劃分
排列
函數 fan x xs 把 x 插入 xs 的每一個可能空隙。例如,fan 1 [2,3,4] 可得到
[[1,2,3,4],[2,1,3,4],[2,3,1,4],[2,3,4,1]]. 讀者不妨想想它該怎麼定義?一種可能方式如下:
fan :: a -> List a -> List (List a)
fan x [] = [[x]]
fan x (y:xs) = (x:y:xs) : map (y:) (fan x xs)
有了 fan, 我們不難定義 perms :: List a -> List (List a), 計算一個串列所有可能
的排列。例如,perms [1,2,3] = [[1,2,3], [2,1,3], [2,3,1], [1,3,2], [3,1,2], [3,2,1]]:
perms :: List a -> List (List a)
perms [] = [[]]
perms (x:xs) = concat (map (fan x) (perms xs))
讀者可思考為何我們需要 concat? 如果沒有,會出現什麼錯誤?
基於 perm 的這個定義,我們證明一個定理:長度為 n 的串列有 fact n 種排列。
這個證明將使用到許多輔助性質與引理,有些已經是我們之前證明過的習題,有些則可作為接下來的習題。
在本證明之中我們也練習將連續的函數應用 f (g (h x)) 寫成函數合成
f . g . h $ x 以方便計算。
定理 2.5
對任何 xs, length (perms xs) = fact (length xs).
在 xs 上做歸納。
基底狀況 xs:=[]:
length (perms [])
=== length [[]]
=== 1
=== fact (length [])
歸納步驟 xs := x:xs:
length (perms (x:xs))
=== {- perms, (.), 與 ($) 之定義 -}
length . concat . map (fan x) . perms $ xs
=== {- 因 length . concat = sum . map length (習題2.12), map 融合 -}
sum . map (length . fan x) . perms $ xs
=== {- 因 length . fan x = (Suc) . length (習題 2.25) -}
sum . map ((Suc) . length) . perms $ xs
=== {- 因 map length (perms xs) =
map (const (length xs)) (perms xs) (習題2.26) -}
sum . map ((Suc) . const (length xs)) . perms $ xs
=== {- 因 sum (map (Suc) xs) = length xs + sum xs (習題2.8)-}
length (perms xs) + sum (map (const (length xs)) (perms xs))
=== {- 因 sum (map (const y) xs) = y * length xs (習題2.9) -}
length (perms xs) + length xs *: length (perms xs)
=== {- 四則運算: x + y * x = (1+y) * x -}
(Suc (length xs)) *: length (perms xs)
=== {- 歸納假設 -}
(Suc (length xs)) *: fact (length xs)
=== {- fact 之定義 -}
fact (Suc (length xs))
=== {- length 之定義 -}
fact (length (x:xs))
練習 2.23
證明 map f . fan x = fan (f x) . map f.
練習 2.24
證明 perm . map f = map (map f) . perm.
練習 2.25
證明 length (fan x xs) = Suc (length xs).
答案
在 xs 上做歸納。基底狀況很容易成立。考慮 xs := y:xs:
length (fan x (y:xs))
=== length ((x:y:xs) : map (y:) (fan x xs))
=== Suc (length (map (y:) (fan x xs)))
=== {- 因 length . map f = length (練習 2.6) -}
Suc (length (fan x xs))
=== {- 歸納假設 -}
Suc (Suc (length xs))
=== Suc (length (y:xs))
練習 2.26
證明 perms xs 傳回的每個串列都和 xs 一樣長,也就是 map length (perms xs) = map (const (length xs)) (perms xs) --- const y 是一個永遠傳回 y 的函數。
答案
欲證明 map length (perms xs) = map (const (length xs)) (perms xs), 在 xs 之上做歸納。基底狀況 xs := [] 中,等號兩邊都化約為 [0]. 考慮歸納步驟 xs := x:xs:
map length (perms (x:xs))
=== {- perms 之定義 -}
map length . concat . map (fan x) . perms $ xs
=== {- map f . concat = concat . map (map f) (習題2.13), map 融合 -}
concat . map (map length . fan x) . perms $ xs
=== {- map length . fan x = (\n -> repeatN n n) . length -}
concat . map ((\n -> repeatN n n) . (Suc) . length) . perms $ xs
=== {- map 融合 -}
concat . map ((\n -> repeatN n n) . (Suc)) . map length . perms $ xs
=== {- 歸納假設 -}
concat . map ((\n -> repeatN n n) . (Suc)) . map (const (length xs)) . perms $ xs
=== {- map 融合 -}
concat . map ((\n -> repeatN n n) . (Suc) . const (length xs)) . perms $ xs
=== {- 歸約 $\lambda$ 算式 -}
concat . map (repeatN (Suc (length xs)) . (Suc) . length) . perms $ xs
=== {- length . fan x = (Suc) . length (習題2.25)-}
concat . map (repeatN (Suc (length xs)) . length . fan x) . perms $ xs
=== {- \todo{???} map (const y) = repeatN y . length -}
concat . map (map (const (Suc (length xs))) . fan x) . perms $ xs
=== {- map f . concat = concat . map (map f) (習題2.13) -}
map (const (length (x:xs))) . concat . map (fan x) . perms $ xs
=== {- perms 之定義 -}
map (const (length (x:xs))) (perms (x:xs))
子串列
函數 sublists :: List a -> List (List a) 計算一個串列的所有子串列。
後者是類似子集合的概念,只是把順序也考慮進去: ys 是 xs 的子串列,
如果將 xs 中的零個或數個元素移除後可得到 ys:
例如 sublists [1,2,3] 的結果可能是: [[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]]。
怎麼定義 sublists 呢?在基底狀況中,空串列仍有一個子串列 [].
歸納步驟中,x:xs 的子串列可分為兩種:不含 x 的,以及含 x 的。
不含 x 的子串列就是 xs 的所有子串列(以下稱作 yss),
而含 x 的子串列就是 yss 中的每個串列接上 x. 因此我們可定義:
sublists :: List a -> List (List a)
sublists [] = [[]]
sublists (x:xs) = yss ++ map (x:) yss
where yss = sublists xs
練習 2.27
定義 splits :: List a -> List (List a × List a),
使 splits xs 傳回所有滿足 ys ++ zs 的 (ys,zs).
例:
&splits [1,2,3] = [([],[1,2,3]), ([1],[2,3]), ([1,2],[3]), ([1,2,3,[]])]另一種說法是 splits xs = zip (inits xs) (tails xs).
答案
splits :: List a -> List (List a × List a)
splits [] = [([],[])]
splits (x:xs) = ([],x:xs) : map ((x:)***id) (splits xs)
其中 (f *** g) (x,y) = (f x, g y), 定義於第 1.6.3 節。
練習 2.28
證明 length . sublists = exp 2 . length. 也就是說,長度為 n 的串列的子串列數目為 $2^n$. 你會需要的性質可能包括 (2.2 ) (length (xs++ys) = length xs + length ys), 以及 length . map f = length.
答案
將式子改寫為 length (sublists xs) = exp 2 (length xs), 在 xs 上做歸納。
歸納步驟 xs := x:xs 為:
length (sublists (x:xs))
=== length (sublists xs ++ map (x:) (sublists xs))
=== {- 因 \eqref{eq:length-append} length (xs++ys) = length xs + length ys -}
length (sublists xs) + length (map (x:) (sublists xs))
=== {- 因 length . map = length (練習 2.6)-}
length (sublists xs) + length (sublists xs)
=== {- 因 x + x = 2 * x -}
2 *: length (sublists xs)
=== {- 歸納步驟 -}
2 *: exp 2 (length xs)
=== exp 2 (Suc (length xs))
=== exp 2 (length (x:xs))
劃分
給定串列 xs :: List a. 如果 yss :: List (List a) 滿足
concat yss = xs,yss中的每個串列都不是空的, 我們便說yss是xs的一個劃分(partition)。 例如,[[1],[2,3],[4]]是[1,2,3,4]的一個劃分。
我們是否能定義一個函數 parts :: List a -> List (List (List a)),
計算一個串列所有的劃分?
可能的方式有很多,其中一種思路如下。首先,空串列 [] 只有一個可能的劃分,即是 [].
考慮 x:xs, 如果 xs 已經被劃分完畢,對於 x 我們有兩個選擇:讓它成為單獨的串列,
或著將 xs 加入最左邊的串列。
寫成歸納定義如下:
parts :: List a -> List (List (List a))
parts [] = [[]]
parts (x:xs) = concat (map (extend x) (parts xs))
where extend x [] = [[[x]]]
extend x (ys:yss) = [[x]:ys:yss, (x:ys):yss]
遞迴呼叫 parts xs 找出 xs 的所有劃分,
輔助函數 extend x :: List (List a) -> List (List (List a)) 則作用在其中一個劃分上。
我們得分出兩個狀況:
如果該劃分含第一個串列
ys和剩下的yss, 我們可選擇讓x自成一個串列,或著加入ys中。 我們把這兩種選擇收集到一個串列中,因此extend的結果有三層List。如果該劃分是空的,加入
x的劃分必定是[[x]],但仍需放入一個串列中,表示「只有這一個選擇」。
呼叫 map (extend x) 的結果需再用一個 concat 聯集在一起。
反函數
假設我們有一個取反函數的運算元:給定 f :: a -> b, 其反函數 inv f 的型別是 b -> List a。
inv f y 將所有使得 f x = y 的 x 收集在串列中。
則 parts 可以有一個更接近本節中文字描述的定義:
parts = filter (all (not . null)) . inv concat
計算 parts xs 時,我們用 inv concat 找出所有滿足 concat yss = xs 的 List (List a), 並且只挑出不含空串列的。
上述定義可以轉換成本節的歸納定義。相關研究可參考 Mu and Bird [2003].