🎯 什麼情境該想到我

當你手上有兩個幾乎一模一樣、只差一兩處的定義時 —— 不論那是兩個函式,還是兩份資料定義。

書中一開頭就點名這種味道:符號表的定義和數表的定義「只有兩處不同:一處是資料型別名,另一處是關鍵字 symbol 和 number」(p.170)。同樣地,兩個在表裡找東西的函式「幾乎無法區別」,唯一的差別只是要找的那個符號不同(p.170)。

為什麼要處理它:

許多程式錯誤出現的原因都是重複,所以好的程式設計者總是盡可能避免重複。(p.170)

書把寫函式類比成寫散文 —— 第一稿只是草稿,函式會被很多人讀、被人改,所以我們必須學會「編輯」函式;而編輯程式的過程中,最主要的步驟就是去除重複(p.170)。

好處有三個,都在書裡明講:

  1. 一個函式同時完成很多任務(p.171)—— 抽象出來的 filter1 不只能做 below / above,還能拿去做等於、小於等於、大於等於(p.173)。
  2. 一處修改,所有人受益(p.175)—— 改進實作、修掉一個邏輯錯誤、甚至加新功能,所有用到它的地方都跟著改進。
  3. 統一了許多輸入的資料型別(p.178)—— 作用在 X 表上就得到 X 表,這種函式書中稱為「多型函式,或者叫做一般的函式」。

⚙️ 怎麼用

語言無關的五步。前三步是第 19 章的做法,第 4、5 步的措辭取自第 22 章那份階段清單(p.195-196)。

1. 並排 —— 比較兩個定義,標記出不同點

把兩個定義排在一起逐行對照。書裡的圖 19.1 就是這樣排的,而且把差異處塗上不同底色:「為了突出這個不同點,在圖中這兩個符號使用了不同的底色。」(p.170)

差異通常小得可疑:

  • 只差一個要比對的值('doll / 'car,p.170)
  • 只差一個運算子(< / >,p.171)
  • 只差一個關係算子,連處理的資料型別都可以不同(below 對數、below-ir 對存貨記錄,p.177)
  • 資料定義只差型別名(數表 / IR 表,p.176)
  • 甚至只差名字 —— 兩個算長度的函式「僅僅是名字不同。如果我們給他們起相同的名字,那麼他們就完全一樣了」(p.178)

2. 把不同之處變成參數

好的程式設計者不會定義多個類似的函式,而是定義單一的、在一個表中既能夠尋找 ‘doll,又能夠尋找 ‘car 的函式。這個更一般的函式需要額外的參數。(p.171)

差異是一個值,就多一個值參數;差異是一個運算子或函式,就多一個函式參數。書中對此有個定名:

將兩個相關的函式結合成一個單獨函式的過程叫做函式抽象。(p.171)

前提是你的語言得允許這件事。第 19 章那些函式「在兩個地方違反了基本的 Scheme 語法」:一是函式和基本操作的名稱被當成參數傳入,二是參數被當成函式使用(出現在呼叫的第一個位置)(p.180)。書的處理是擴充語法、並讓值的集合包含函式名與基本操作名,因為 ——

如果沒有這樣的概念,就不可能對函式進行抽象。(p.180)

3.(替代做法)用 local 把差異「記住」,回傳一個函式

第 22 章給了不同於第 21 章的第二種訣竅:不把差異放進參數表,而是把原來那個具體函式包進 local,用它的名字當 local 的主體,再把差異的名字列成外層的參數表(p.195)。

(define (abs-fun op1 op2)
  (local ((define (concrete-fun x y z)
            ... op1 ... op2 ...))
    concrete-fun))

回傳的函式會永久地記住傳進來的那個差異(p.194-195)。另外書有一條命名提醒:「如果 op1 或者 op2 是個特別的符號,比如 <,我們就在新的環境中給它起一個更有意義的名稱。」(p.195)

4. 測試 —— 用抽象函式反過來定義原來那兩個

為了測試抽象函式,我們仍然使用抽象的函式反過來定義原來的函式。(p.195)

(define below2 (filter2 <))(define above2 (filter2 >)) —— 只要把抽象函式作用在「原先具體函式中不同之處的東西」上,就該得回原來的函式(p.196)。第 19 章也是同一招:below1 產生和 below 相同的結果,above1 產生和 above 相同的結果,而且「只使用一行程式碼」(p.173)。

5. 改合約 —— 把具體型別換成型別變數

這步是整張卡最容易被跳過、也最該做的。書自己也承認它一開始跳過了:「現在,我們還不知道如何寫出像 filter1 這樣的函式的合約。我們先跳過有關合約的問題」(p.172),到第 20 章才補上。

三個要點:

(a) 函式參數用箭頭型別寫。 (A B -> C) 表示「讀入一個 A 型別的元素和一個 B 型別的元素,回傳 C 型別的元素」,即「把 A 和 B 映射到 C」的函式(p.181)。所以 filter1 的第一版合約是 (number number -> boolean) lon number -> lon(p.182)—— 它的與眾不同之處在於,第一個參數的型別不是資料定義中引入的名稱,而是用箭頭符號直接定義的(p.182)。

(b) 具體資料型別換成型別變數。

記號 ITEM(元素)是一種型別變數,代表任意 Scheme 資料的集合,包括:符號,數,布林值,IR……(p.178)

把 ITEM 換成某個具體資料的名稱,就得到一個具體實例;配合縮寫 (listof ITEM),於是 (listof symbol) 是所有符號表、(listof number) 是所有數表、(listof (listof number)) 是由數表構成的表(p.178)。合約裡也可以直接用 X:「X 只是一個變數,代表某種資料型別的名字。」(p.179)

(c) 同一個變數在所有位置必須被同一樣東西代替。 filter1 的一般化合約是:

;; filter1 : (X number -> boolean) (listof X) number -> (listof X)
我們可以用任何東西代替這個 X,只要三個 X 出現的地方都被同一樣東西代替。(p.182)

而且每一個獨立變動的東西都該有自己的變數。當「限值」參數從數變成符號時,合約就矛盾了,解法是給限值再引入一個變數 TH(p.183):

;; filter1 : (X TH -> boolean) (listof X) TH -> (listof X)

書最後把型別收成四類:基本型別;定義的型別;函式型別;以及參數型別 —— 「要麼是定義的型別,要麼是含有型別變數的函式型別」(p.184)。

🧪 我實際套用的紀錄

  • (待填)

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

  • 合約寫不出來,就是抽象沒站住。 書給了一條明確的收手規則:「如果要使用含有參數型別的函式,我們必須先找到一個(所有函式合約中變數的)替換,使得所有的參數都屬於合適的型別。如果做不到這一點,我們要麼修改函式的合約,要麼認為這個函式不適用於這種情況。」(p.184)—— 兩個出口都是誠實的,不要硬套。
  • 這套消除相似性的方法有語言前提。 「這裡消除類似的方法僅針對像 Scheme 這樣的函式式程式設計語言;不過,其他類型的語言,特別是物件導向語言,支持類似去除相似性的機制 —— 有時候這種機制被稱作模式。」(p.170)換語言就換手法,別直接照搬。
  • 語言得先讓函式成為值。 見上面第 2 步:沒有「函式名和基本操作名也是一種值」(p.181),這條路走不通。
  • 不是每個抽象都值得慶祝。 書自己吐槽:「就 contains-doll? 和 contains-car? 而言,函式抽象一點也不好玩。」(p.171)真正有趣的是差異落在運算子/函式上的那種(圖 19.2),因為抽象出來後能移作它用。
  • 抽象完可能還能再抽象一次。 filter1 一開始固定要「拿元素跟第二個參數比」,後來發現關係函式可以忽略它的第二個參數,於是簡化成只讀入一個判斷函式和一個表的 filter(p.174-175)。第一版抽象不是終點。
  • 抽象出來的東西效能不會自動變好。 書在習題 19.1.5 直接問:「對於比較長的表,為什麼這兩個函式執行起來非常慢?」並要你引入區域名稱記住自然遞迴的結果來改進(p.176)。抽象是結構問題,效能要另外解。
  • 「該抽象什麼」本身沒有訣竅可循。 這是書最誠實的一句話:抽象函式往往比你原本想要的功能廣泛得多,但「不幸的是,並不存在一個通用的訣竅可以指導我們發現這類功能,我們所能做的只是多實踐,並且留心觀察哪裡適合使用抽象函式。」(p.186)—— 上面那套步驟只能在你已經看到兩個相似定義之後幫你;看不看得到,得靠練習。

🔗 相關工具

  • 工具-設計訣竅 —— 同一本書的前置步驟。訣竅產出的模板決定了函式的基本結構,所以「讀入相同資料型別的函式看起來很類似」(p.170);這張卡處理的正是訣竅產出的那一堆相似函式
  • 工具-資料驅動的模板推導 —— 相似性的來源。書指出資料定義相似,讀入它們的函式也會相似(p.177),所以資料定義層的抽象(參數資料定義、(listof ITEM))和函式層的抽象是同一件事的兩面
  • 工具-提煉函式 —— 出自《重構》,形狀像但方向不同:提煉函式是把一段內聚的程式碼抽出來命名,處理「太長 / 需要註解解釋」;這張卡是把兩個完整的定義合併成一個,處理「長得太像」,而且多了一步《重構》沒有的功課 —— 合約要改用型別變數
  • 程式設計方法 —— 出處,第 19、20、22 章