🎯 什麼情境該想到我

當你「已經知道輸入長什麼樣,卻不知道函式的骨架該怎麼寫」的時候。

書把這件事講得很直白:「從資料定義到模板是設計函式過程中的主要步驟」,而且**「函式輸入的資料定義往往在很大程度上決定函式的形狀」(p.79)。骨架不是想出來的,是從資料定義推**出來的。


⚙️ 怎麼用

先寫下資料定義,再照下面的規則機械地把模板抄出來。三種資料型態,三條規則。

規則一:看到「複合資料(結構體)」→ 模板就是一排選擇器,沒有分支

資料有 N 個欄位 → 模板主體就列出 N 個選擇器表達式,一個欄位一行
沒有 cond,因為資料定義裡沒有「兩者之一」
剩下的問題只有一個:「這 N 個值要怎麼組成答案」
  • 資料分析時就對齊了:「如果發現一個對象有 N 個屬性,可以引入一個有 N 個字段的結構體」(p.37)
  • 反過來推模板:「parent 的結構體定義指定了四個字段,所以模板中有四個表達式」(p.127)
  • 這個模板不看輸出,所以可以重複用、而且可以在寫例子之前就寫好

「模板包括函數頭部和主體,主體中列出了所有可能的選擇器表達式。」
「換句話說,模板表示了程序對於輸入的了解,並不涉及輸出。所有輸入相同的函數可以使用相同的模板。另外,由於模板與目的無關,可以在例子之前闡述。」(p.38)

圖 6.5 把這條規則壓成一句話:「若參數是複合資料,使用選擇器表達式填寫主體 / 如果函數是條件式的,寫出所有合適的分枝」(p.40)。

規則二:看到「自引用資料定義」→ 模板必然是兩支

1. 資料定義有幾個子句,cond 就有幾支
2. 不含自引用的那一支 = 基本情況(空 / 沒有下一層)→ 直接給答案
3. 含自引用的那一支:先用選擇器把各部分取出來,
   再對「自引用的那一欄」呼叫函式自己
然後只剩一個問題:「怎麼把這兩者的值合起來」

書用三個小步驟示範這條推導(p.78-79 的 sum):

  1. 「第一步,添上 cond 表達式」
  2. 「第二步,為每個子句添上合適的選擇器表達式」
  3. 「最後一步,添上遞迴,也就是處理資料定義中的自引用部分」

分支數量不是憑感覺定的:

「自引用的數據定義所描述的是混合數據類型,其中每個子句描述一種子類型……特別地,每個條件子句與一種數據定義相對應,因此 cond 表達式的子句數應該和數據定義的子句數一樣。」(p.76)

遞迴呼叫的位置也不是憑感覺定的——書要你逐條檢查選擇器:

「另外,檢查每一個選擇器表達式,如果某個選擇器表達式的返回值類型與函數的輸入數據類型一致,就用箭頭把它和函數的參數連接起來。最後得到的箭頭數目必然與數據定義中的箭頭數目相同。」(p.76)

書把這種自呼叫命名為**「自然遞迴」**(p.76)。

寫主體的順序也被定死了:「設計主體從不包含自然遞迴的那些 cond 子句開始。這些子句被稱為基本情況,它們所對應的答案一般已由例子給出,或者很容易得到。」 然後才處理自引用那支,而且**「對於那些遞迴調用,我們假設函數已經能夠按照我們指定的用途說明工作,剩下的問題就是把不同的值結合起來」**(p.76)。

那個「合起來」的動作,書也給了選項:「在許多情況下,可以使用 Scheme 的基本操作,如 +,and 或 cons。如果問題描述包含了對第一個元素的處理說明,我們可能還需要使用嵌套的 cond 語句。最後,在某些情況下,可能還需要定義輔助函數。」(p.77)

圖 9.2 的模板列就是這條規則的檢查清單(p.77):

對於每種可能情況都設計一個 cond 表達式
對於每個子句添加選擇器
將主體標記為遞迴
測試模板中的自引用是否與數據定義匹配

規則三:看到「相互引用的資料定義」→ 一次生出一組互相呼叫的函式

有幾個互相引用的資料定義,就同時開幾個模板、幾份合約
每個模板各自照規則一或規則二寫(該有 cond 的才有 cond)
資料定義之間的箭頭 → 模板之間的互相呼叫
資料定義之內的箭頭 → 該模板的自我遞迴
從「既沒有自引用、也沒有相互引用」的那個子句開始填答案

書說這只是前兩條規則的一般化,只多兩件事:

「對自引用數據定義的函數設計訣竅一般化,就可得到相互引用數據定義的函數設計訣竅。事實上,為了處理相互引用的數據定義,只需要增加兩條建議。第一條,我們必須同時建立多個模板,每個模板對應一個數據定義;第二條,我們必須用自引用和相互引用來註釋模板,所謂相互引用,就是不同模板之間的相互引用。」(p.129)

函式的數量也由資料定義的數量決定:「要處理數據類型的相互聯繫,需要和數據定義數量一樣多的函數。因此,我們必須並行地給出和數據定義一樣多的合約、用途說明以及頭部。」(p.129)而**「模板須遵循關於複合數據、混合數據以及自引用數據的建議並行建立」**(p.129)——也就是說規則一、規則二沒有被取代,只是被同時套用。

書的示範清楚說明「有沒有 cond」完全看資料定義(p.130):

「fun-parent 模板中沒有條件,因為 parent 的數據定義中並不包含任何子句,而是包含對第二個模板的相互引用:處理 parent 結構體的 children 字段。按照同樣的規則,fun-children 是一個條件式,第二個 cond 子句中包含了一處自引用,處理表的 rest 部分,以及對表的 first 元素——即 parent 結構體——的一處相互引用。」

寫出來的成品就是一組互相呼叫的函式:「與其他函數群體不同,這兩個函數相互引用,是相互遞迴的。毫不令人驚奇的是,函數定義中的相互引用對應於數據定義中的相互引用。」(p.128)

圖 15.4 的清單(p.130):

同時開發與數據定義一樣多的模板
·遵照複合數據和/或混合數據的規則正確開發每一個模板
·根據數據定義中的(相互)引用,用遞迴和相互調用註釋模板

(主體)給出每一個模板和模板中 cond 子句的 Scheme 表達式
·解釋模板中的每個表達式計算出什麼
·在需要時,使用額外的輔助函數

填答案的起點同樣被指定了:「在開始創作最後的定義時,我們要從一個模板或 cond 子句開始,這個模板中應該不包含自引用以及對其他模板的相互引用。對於這樣的模板或 cond 子句,結果一般較為易於給出。」(p.130)

三條規則的共同形狀

資料定義長什麼樣模板必然長什麼樣
N 個欄位的結構體N 個選擇器表達式,無 cond
「下列兩者之一」+其中一支引用自己cond 兩支;非自引用支=基本情況;自引用那一欄→遞迴呼叫自己
多個定義互相引用多個模板同時建立;定義間的引用→函式間的互相呼叫

推完模板之後,永遠只剩同一個問題:把手上這幾個值結合成答案。


🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

書中明講的限制:

  • 模板不編碼欄位的型別。 書自己點名了這個缺口:「最終的模板幾乎已經包含了數據定義的每一個方面:兩個子句,第二個子句中的 cons 結構,以及第二個子句中的自引用。數據定義中唯一沒有在函數模板中得到反映的部分是,cons 結構的第一個部分是數。」(p.79)→ 模板保證形狀對,不保證你沒把符號當數字用。
  • 模板不告訴你輸出。 「模板表示了程序對於輸入的了解,並不涉及輸出。」(p.38)→ 推導只給骨架,答案還是要自己想;也因此**「所有輸入相同的函數可以使用相同的模板」(p.38),而「模板可以被重用多次,這也意味著例子的構造應該在模板設計之後」**(p.39)。
  • 遞迴資料定義本身要合法,規則二才有意義。 「要使遞迴的數據定義有意義,必須滿足兩個條件:第一,該定義必須至少含有兩條子句;第二,其中至少有一條子句不能引用定義自身。」(p.76)→ 資料定義寫壞了,推出來的模板也是壞的(沒有基本情況=沒有出口)。
  • 模板只管到骨架,複雜的地方還是要拆。 圖 9.2 與圖 15.4 都留了同一句:「在某些情況下,可能還需要定義輔助函數」(p.77)/「在需要時,使用額外的輔助函數」(p.130)。
  • 自引用資料的觸發條件是「任意長」。 「若問題描述涉及任意長的複合信息,就需要使用遞迴或者是自引用數據。」(p.75)→ 長度固定的東西不該套規則二;書處理三個數的表時,用的是三層 first/rest 的直線模板而非遞迴(p.71)。

我從書中內容直接推出的邊界(非書中原文):

  • 這套推導的前提是你已經先寫出資料定義。書把「資料分析和設計」排在模板之前(圖 6.5 p.40、圖 9.2 p.77、圖 15.4 p.130),資料定義沒寫,就沒有東西可推。
  • 資料定義本身是可以被違反的——書的註腳提醒「數據定義事實上只是一份書面意圖聲明,但任何有意或無意違反該聲明的人都將面臨異常的計算結果」(p.36)。所以模板推導的正確性,上限就是資料定義的誠實程度。

🔗 相關工具

  • 工具-設計訣竅 —— 本卡是設計訣竅裡「模板」那一步的放大版。書中三張表都是同一份訣竅的精化:圖 6.5 標明是「圖 2.2 中設計訣竅的精化」(p.40),圖 15.4 註明「基本步驟;其他的步驟請參見圖 2.2、圖 6.5 及圖 7.3」(p.131)。合約、例子、測試那幾步請看那張卡。
  • 工具-生成遞迴與終止論證 —— 這張卡處理的是由資料定義驅動的結構遞迴(遞迴呼叫的位置由自引用欄位決定,因此不必另外論證會終止);那張處理的是另一種遞迴。註:本卡的三個來源章節(第 6、9、15 章)並未討論那種遞迴,兩者的關係說明由卡片維護者標注,不出自本卡引用的頁面。
  • 回連 程式設計方法