2.7 再談「讓符號為你工作」
看了這麼多歸納定義,我們做個較複雜的練習,並以此為例再談談「讓符號為你工作」。 本節在初次閱讀時可略過。
以下的函數 ins 將一個元素插到一個串列的空隙中。它和第2.6.4 節中的 fan 類似但稍有不同 --- ins x xs 不會把 x 放到 xs 的結尾。
本節不使用 fan 而另外定義了 ins 的原因僅是因為如此使得性質 (2.7 ) 較單純而方便說明。
例如:ins 1 [2,3,4] = [[1,2,3,4],[2,1,3,4],[2,3,1,4]]。
ins :: a -> List a -> List (List a)
ins x [] = []
ins x (y:ys) = (x:y:ys) : map (y:) (ins x ys)
請證明對所有 x, ys, 與 zs,
map (++zs) (ins x ys) ++ map (ys++) (ins x zs) &= ins x (ys ++ zs) (2.7 )
意即:在 ys ++ zs 之中插入 x 的所有方法,
是 1. 把 x 插入 ys 之中,並把每個結果的右邊接上 zs,
以及 2. 把 x 插入 zs 之中,並把每個結果的左邊接上 ys.
面對一個如此冗長的式子,我們該如何著手?我們可猜想如此複雜的性質大概會需要歸納證明。
但在哪個變數上做歸納呢?
觀察(2.7 ),等號左手邊最外側的運算子是(++)。
計算 (++) 時,根據定義,先被樣式配對的是 (++) 左邊的參數,也就是 map (++zs) (ins x ys)。
再來,根據 map 的定義,map (++zs) (ins x ys) 的運算要有進展,得先把 ins x ys 算出來。
最後,根據 ins 的定義,計算 ins x ys 會先拆開 ys.
由此可知,等號左手邊的運算是從拆解 ys 開始的。
等號右手邊的情況也一樣:根據 ins 與 (++) 的定義,要計算 ins x (ys ++ zs),第一個被拆開的是 ys. 因此,欲證明(2.7 ),合理的猜測是在 ys 上做歸納!
我們省略 ys := [] 的情況,僅考慮歸納情況 ys := y:ys。
經代換後的式子是 map (++zs) (ins x (y:ys)) ++ map ((y:ys) ++) (ins x zs).
我們採取的策略和證明定理2.1 時一樣:
想辦法湊出 map (++zs) (ins x ys) ++ map (ys++) (ins x zs), 以便使用歸納假設。
由此論證如下(步驟加上編號以利說明):
map (++zs) (ins x (y:ys)) ++ map ((y:ys) ++) (ins x zs)
=== {- 1. ins 與 map 之定義 -}
(x:y:ys++zs) : map (++zs) (map (y:) (ins x ys)) ++ map ((y:ys) ++) (ins x zs)
=== {- 2. map 融合 -}
(x:y:ys++zs) : map ((++zs) . (y:)) (ins x ys) ++ map ((y:ys) ++) (ins x zs)
=== {- 3. 由於 (++zs) . (y:) = (y:) . (++zs) (習題 2.30) -}
(x:y:ys++zs) : map ((y:) . (++zs)) (ins x ys) ++ map ((y:ys) ++) (ins x zs)
=== {- 4. map 融合 -}
(x:y:ys++zs) : map (y:) (map (++zs) (ins x ys)) ++ map ((y:ys) ++) (ins x zs)
=== {- 5. 由於 ((y:ys)++) = (y:).(ys++) (習題 2.29) -}
(x:y:ys++zs) : map (y:) (map (++zs) (ins x ys)) ++ map ((y:).(ys++)) (ins x zs)
=== {- 6. map 融合 -}
(x:y:ys++zs) : map (y:) (map (++zs) (ins x ys)) ++ map (y:) (map (ys++) (ins x zs))
=== {- 7. 由於 map f (xs ++ ys) = map f xs ++ map f ys -}
(x:y:ys++zs) : map (y:) (map (++zs) (ins x ys) ++ map (ys++) (ins x zs))
=== {- 8. 歸納假設 -}
(x:y:ys++zs) : map (y:) (ins x (ys ++ zs))
=== {- 9. ins 與 (++) 之定義 -}
ins x ((y:ys)++zs)
雖然步驟多、式子長,但策略確定後,整個證明的架構便很清楚。第 1 到 7 步都是為了可在第 8 步使用歸納而做的準備。
為湊出 map (++zs) (ins x ys) ++ map (ys++) (ins x zs), 第 2 至第 4 步把 map (++zs) 往裡推,把第一個 map (y:) 往外提;第 5, 6 步則提出另一個 map (y:)。式子變成 map (y:) ... ++ map (y:) ... 的形式後,共同的 map (y:) 可在第 7 步提出來。
如此,終於可使用歸納假設了。
整個證明中,要配對哪個變數、把式子中的哪些項往哪兒移動,都被符號引導著而有線索可循。
證明中用到的兩個性質 (++zs) . (y:) = (y:) . (++zs) 與 ((y:ys)++) = (y:).(ys++) 並非天外飛來,而是確立了目標後,因為有需求(兩者分別讓我們把 (++zs) 與 (ys++) 往右推,把 (y:) 往左拉)而設計出並另外證明的性質。
初學者常犯的一個錯誤是:以為所有情況下、所有的變數都需要樣式配對。
事實上,過度的樣式配對是形式證明中希望避免的。
藉由觀察 (2.7 ),我們決定對 ys 做歸納,證明過程中發現如此便已經足夠。
如果我們對 ys 和 zs 都做配對,將必須處理四種情況:ys,zs := [],[],[], ys,zs := [],z:zs, ys,zs := [],z:zs ys,zs := y:ys,z:zs。一個待證明的性質若有 n 個串列,似乎得分出 $2^n$ 種情況 --- 只有萬不得已我們才會這麼做。
最嚴重的問題並不是情況數目太多,而是過度的樣式配對破壞了證明的結構。
如果對 zs 做配對,許多人會很自然地想把 ins x (z:zs) 展開。
證明可能如此進行:
map (++(z:zs)) (ins x (y:ys)) ++ map ((y:ys) ++) (ins x (z:zs))
=== {- 將 (++) 的左手邊展開 -}
(x:y:ys++(z:zs)) : map (++(z:zs)) (map (y:) (ins x ys)) ++
map ((y:ys) ++) (ins x (z:zs))
=== {- 展開 ins x (z:zs) -}
(x:y:ys++(z:zs)) : map (++(z:zs)) (map (y:) (ins x ys)) ++
map ((y:ys) ++) ((x:z:zs) : map (z:) (ins x zs))
=== {- map 的定義 -}
(x:y:ys++(z:zs)) : map (++(z:zs)) (map (y:) (ins x ys)) ++
((y:ys ++ (x:z:zs)) : map ((y:ys) ++) (map (z:) (ins x zs)))
要從這樣的式子中再整理出
map (++(z:zs)) (ins x (y:ys)) ++ map ((y:ys) ++) (ins x (z:zs))
以便做歸納假設,
我們得看出如何把(本來不需展開的)ins x (z:zs) 給收回來。
式子的結構已經被破壞,操作起來的難度便大大提高了。
這其實是我於 2019 年開設的程式語言課程的期末考題。在十數位將 ins x (z:zs) 展開的同學中,只有一位成功地將它收了回來。
練習 2.29
證明:對所有 y 與 ys, (y:).(ys++) = ((y:ys)++).
答案
((y:).(ys++)) xs
= {- definition of (.) -}
y : (ys ++ xs)
= {- definition of (++) -}
(y : ys) ++ xs
練習 2.30
證明:對所有 y 與 zs, (++zs) . (y:) = (y:) . (++zs).
以上兩者都是不需歸納、也不須狀況分析的單純證明。
答案
((++zs) . (y:)) xs
= {- definition of (.) -}
(++zs) (y:xs)
= (y:xs) ++ zs
= {- definition of (++) -}
y :(xs ++ zs)
= (y:) ((++ zs) xs)
= {- definition of (.) -}
((y:) . (++zs)) xs