5 一般程式推導

5.1 展開-收回轉換

考慮這麼一個例子:給定一個整數形成的串列,我們想計算其每個數的平方的和。例如當輸入是[2,6,5,3], 我們希望算出 $2^2 + 6^2 + 5^2 + 3^2 = 74$. 這項工作可以簡短地描述如下:

函數 sumsq 的型別為 List Int -> Int. 其中 map square 將輸入串列的每個元素都平方,然後由 sum 計算其總和。第2.4 節中給過一個 sum 的歸納定義,重複如下:

sum :: List Int -> Int
sum []      = 0
sum (x:xs)  = x + sum xs 

消除中間串列   對大部分的應用而言,如上定義的 sumsq 已經很堪用了。但作為一個例子,我們來挑惕些仍不滿意之處。執行 sumsq xs 時,map square 會產生另一個(存放 xs 每個元素的平方的)串列,該串列隨即由 sum 消掉 --- 感覺上似乎很浪費空間與時間。 在惰性求值的情況下,該中間串列的每個節點被產生後立刻被 sum 消去,因此不會真的佔用和 xs 同樣長度的空間。這也是大家覺得惰性求值有助於模組化、使小函數變得易於重用的例子之一。但「產生一個新節點、立刻消去」仍耗了一些不必要的時間。 有不產生這個中間串列的方法嗎?

我們試著做些計算。當 sumsq 的參數是 [] 時:

   sumsq []
 ===    {- sumsq 之定義 -}
   sum (map square [])
 ===    {- map 之定義 -}
   sum []
 ===    {- sum 之定義 -}
   0 

由此我們得知 sumsq [] 會被計算成 0. 當輸入不是空串列時呢?試計算:

   sumsq (x:xs)
 ===    {- sumsq 之定義 -}
   sum (map square (x:xs))
 ===    {- map 之定義 -}
   sum (square x : map square xs)
 ===    {- sum 之定義 -}
   square x + sum (map square xs)
 ===    {- sumsq 之定義 -}
   square x + sumsq xs 

可得知對任何 x 與 xs, sum (x:xs) 歸約的結果和 square x + sumsq xs 會是相同的。 總結說來,藉由計算,我們發現 sumsq 滿足以下兩條性質:

但如果我們翻轉過來,把這兩條性質當作 sumsq 的新定義呢? 這是一個依據歸納法定義的良好程式,而且不會產生中間串列!

但我們怎知道 sumsq 的新定義滿足我們最初的要求 ([RefUndefined]),即 sumsq = sum . map square 呢? 回顧起來,我們的演算只證明了當 sumsq 滿足 ([RefUndefined]),它也滿足 ([RefUndefined]), 卻還不能據此宣稱另一個方向:若 ([RefUndefined]) 成立,([RefUndefined]) 也成立。 我們將在第4 章詳細討論這個問題。 目前可暫時這麼說:如果我們從某個問題規格 f = e 起始,發現 f 滿足某一組等式,而這些等式剛好可湊成一個會正常終止的歸納定義,則 e 確實是唯一滿足這些等式的解。由於如此的 e 是唯一的,我們也可倒過來以這組等式為 f 的定義,並同時宣稱 f = e 這個性質成立。

回顧起來,在 sumsq 的計算中,我們僅是把 sumsq 的定義展開,接著展開 map, sum 等等元件的定義,直到我們又看到 sumsq 的定義出現在式子中、剛好可以收回為止。 這是一種單純而歷史悠久的程式推導方法,稱作展開-收回轉換(fold-unfold transformation)。 雖然簡單,有時這個方法意外地有用。

在這個例子中,我們真正做到的是將一個單行、使用全麥編程的 sumsq 定義轉換成了一個歸納定義。 新定義的 sumsq 比起原版稍有效率些,但這只是恰巧發生的 --- 歸納定義的程式不見得總會比較有效率。 程式推導確保程式的正確性 --- 意即導出的程式與原本的規格是同一個函數。 但新程式的效率仍須單獨分析。這是下一節的主題。

練習 5.1

下述函數 descend n 傳回 [n, n-1, n-2...0]:

descend :: Nat -> List Nat
descend Zero     = []
descend (Suc n)  = Suc n : descend n 

定義 sumseries = sum . descend. 請找出 sumseries 的歸納定義。

答案

顯然 sum (descend 0) = 0. 考慮歸納情況:

   sum (descend (Suc n))
 ===   {- descend 之定義 -}
   sum (Suc n : descend n)
 ===   {- sum 之定義 -}
   (Suc n) + sum (descend n)
 ===   {- sumseries 之定義 -}
   (Suc n) + sumseries n 

因此

sumseries Zero     = Zero
sumseries (Suc n)  = (Suc n) + sumseries n 
練習 5.2

承接習題5.1 。函數 repeatN :: (Nat × a) -> List a 的定義為

repeatN (n,x) = map (const x) (descend n) 

因此,repeatN (n,x) 會傳回一個含 n 個 x 的串列。 例如 repeatN (3,'a') = "aaa". 請算出一個歸納定義的 repeatN.

答案

顯然 repeatN (0,x) = []. 至於歸納情況,演算如下:

   repeatN (Suc n, x)
 ===   {- repeatN 之定義 -}
   map (const x) (descend (Suc n))
 ===   {- descend 之定義 -}
   map (const x) (Suc n : descend n)
 ===   {- map 與 const 之定義 -}
   x : map (const x) (descend n)
 ===   {- repeatN 之定義 -}
   x : repeatN (n,x) 

因此,

repeatN (Zero,   x)  = []
repeatN (Suc n,  x)  = x : repeatN (n,x) 
練習 5.3

承接習題5.2 。遊程編碼(run-length encoding)是一種簡單的壓縮方式:將字串中重複的字元表達成其出現的數字。例如 "aaabbbbcdd" 可以表達為 "3a4b1c2d". 下列函數 rld :: List (Nat × a) -> List a 則是抽象過的「遊程解碼」,將已經表示成(次數$\times$字元)的壓縮文展開:

rld = concat . map repeatN 

例如, rld [(2,'a'), (3,'b'), (1,'c')] = "aabbbc". 請導出 rld 的歸納定義。

答案

基底狀況:

   rld []
=    {- rld 之定義 -}
   concat (map repeatN [])
=    {- map 與 concat 之定義 -}
   [] 

歸納狀況:

      rld ((n,x):xs)
 ===    {- rld 之定義 -}
      concat (map repeatN ((n,x):xs))
 ===    {- map 之定義 -}
      concat (repeatN (n,x) : map repeatN xs)
 ===    {- concat 之定義 -}
      repeatN (n,x) ++ concat (map repeatN xs)
 ===    {- rld 之定義 -}
      repeatN (n,x) ++ rld xs 

因此我們已推導出:

rld []          = []
rld ((n,x):xs)  = repeatN (n,x) ++ rld xs 
練習 5.4

下列函數 delete 將輸入串列中的每個元素輪流刪除:

delete         ::  List a -> List (List a)
delete []      =   []
delete (x:xs)  =   xs : map (x:) (delete xs) 

例如,delete [1,2,3,4] = [[2,3,4], [1,3,4], [1,2,4], [1,2,3]]. 函數 select :: List a -> List (a × List a) 則將一個串列中的元素依次選出。 例如,select [1,2,3,4] = [(1,[2,3,4]), (2,[1,3,4]), (3,[1,2,4]), (4,[1,2,3])]. 函數 select 恰巧可用 delete 定義出來:

select xs = zip xs (delete xs) 

請推導出 select 的歸納定義。提示:下述性質可能有用 --- zip xs (map f ys) = map (id *** f) (zip xs ys).

答案

顯然 select [] = []。考慮 xs := x:xs 的情況:

    select (x:xs)
 ===   {- select 與 delete 之定義 -}
    zip (x:xs) (xs : map (x:) (delete xs))
 ===   {- zip 之定義 -}
    (x, xs) : zip xs (map (x:) (delete xs))
 ===   {- zip xs (map f ys) = map (id *** f) (zip xs ys) -}
    (x, xs) : map (id *** (x:)) (zip xs (delete xs))
 ===   {- select 之定義 -}
    (x, xs) : map (id *** (x:)) (select xs) 

因此,

select []      = []
select (x:xs)  = (x, xs) : map (id *** (x:)) (select xs) 
練習 5.5

函數 delete 有另一個可能定義:

delete xs = map (del xs) [0..length xs-1]
   where del xs i = take i xs ++ drop (1+i) xs 

(此處我們利用了當 n 為負數時,[0..n] 化簡成 [] 的特性。) 請用此定義推導出 delete 的歸納定義。提示:你可能用得上下述性質:

  [0..n] = 0 : map (1+) [0..n-1]\mbox{,~~ if n >= 0,}    (5.1 )
答案

以下只列出歸納狀況:

    delete (x:xs)
 ===   {- delete 之定義 -}
    map (del (x:xs)) [0..length (x:xs) -1]
 ===   {- length 之定義,簡單運算 -}
    map (del (x:xs)) [0..length xs]
 ===   {- 由於 length xs >= 0, 使用 \eqref{eq:gen-split} -}
   map (del (x:xs)) (0 : map (1+) [0..length xs-1])
 ===   {- map 之定義 -}
   del (x:xs) 0 : map (del (x:xs)) (map (1+) [0..length xs-1])
 ===   {- map 融合 -}
   del (x:xs) 0 : map (del (x:xs) . (1+)) [0..length xs-1] 

在此暫停一下,觀察 del (x:xs). 顯然, del (x:xs) 0 = xs. 至於 del (x:xs) . (1+),我們演算看看:

    (del (x:xs) . (1+)) i
 ===    {- (.) 與 del 之定義 -}
    take (1+ i) (x:xs) ++ drop (1+ (1+ i)) (x:xs)
 ===    {- take 與 drop 之定義 -}
    x : take i xs ++ drop (1+ i) xs
 ===    {- del 之定義 -}
    x : del xs i
 ===    {- (.) 之定義 -}
    ((x:) . del xs) i 

繼續之前的演算:

    del (x:xs) 0 : map (del (x:xs) . (1+)) [0 .. length xs - 1]
 ===   {- 前述之演算 -}
    xs : map ((x:) . del xs) [0 .. length xs - 1]
 ===   {- map 融合 -}
    xs : map (x:) (map (del xs) [0 .. length xs - 1])
 ===   {- delete 之定義 -}
    xs : map (x:) (delete xs) 

由此,我們導出了正文中 delete 的歸納定義.