🎯 什麼情境該想到我
當你面對一個新函式/問題,不知道從何下手、容易憑感覺亂寫時。
書把這件事的起因講得很清楚:程式設計要考慮的步驟很多——判斷問題描述中哪些資訊相關、釐清輸入輸出與它們的關係、查明語言有沒有現成的基本操作、寫完還要驗證測試——而**「要解決這些表面上混亂的情況,必須建立一些程式設計原則,即,規定完成任務的順序以及每步怎麼做」**(p.8)。
還有一個更現實的理由:語法錯誤和執行錯誤有程式設計環境幫你抓,但邏輯錯誤不會。書上把 wage 寫成 (+ 12 h) 的例子,每次執行都會產生一個數值,甚至 (wage 12/11) 還會剛好算對——「只有細心和系統化地設計程式,程式設計者才能捕獲到此類錯誤」(p.8)。
⚙️ 怎麼用
全書的骨架:六個步驟(前言 圖 0.1)
書在前言就把完整的訣竅攤開了,而且明說**「圖 0.1 給出的設計訣竅包含了 6 個程式設計基本步驟,每個步驟都將產生定義明確的中間結果」**(前言 p.II):
| # | 步驟(圖 0.1) | 產生的中間結果(前言 p.II) |
|---|---|---|
| 1 | 問題分析和資料定義 | 問題資料型別描述 |
| 2 | 合約,用途說明與結果的描述,函數頭部 | 程式行為的非形式描述 |
| 3 | 例子 | 說明程式行為的例子 |
| 4 | 函數模板 | 開發程式的模板或視圖 |
| 5 | 函數定義 | 把模板轉換成完整的定義 |
| 6 | 測試 | 通過測試發現錯誤 |
第 2 章的入門版:圖 2.2
「基於到目前為止所得到的經驗,設計一個程式至少需要如下 4 個步驟」(p.8)
第 2 章處理的是「輸入是數、輸出是數」這種最單純的情況,這時還沒有資料定義可分析、也還沒有模板可推,所以圖 2.2「設計訣竅一覽」只剩四個階段:合約/用途說明和函數頭部/例子/主體/測試(p.10)。下面逐階段附上圖 2.2 的目標與任務欄——這是最容易上手的入門版本,第 1 步與第 4 步等資料變複雜之後才會長出來(見文末「相關工具」)。
1. 合約(contract)
目標:給函數命名
任務:給函數起一個合適的名字(圖 2.2,p.10)
「在開發程式時應該給每一個程式一個有意義的名字,並且說明輸入資料和所產生的資料的類型,這稱為程式的合約」(p.8)。合約寫成一行註解,冒號左邊是名字、右邊是輸入與輸出的類型,中間用箭頭隔開:
;; area-of-ring : number number -> number(p.8。書的註腳:輸入箭頭的方法是先鍵入 - 再鍵入 >。)
2. 用途說明和函數頭部
目標:指定輸入和輸出的類型/描述函數的用途說明/闡明函數頭部
任務:以函數需要的未知數為線索研究問題;給每個輸入起一個名字,如果可能的話,使用在問題描述中給定的名字;使用選擇的變數名描述函數應該產生什麼結果;闡明合約和函數頭部(圖 2.2,p.10)
圖 2.2 直接給了要填的骨架(p.10):
;; name : number ...--> number
;; to compute ... from x1 ...
(define (name x1 ...) ...)- 函數頭部**「複述了程式的名字,同時給每個輸入一個不同的名字,它們是(代數)變數,是程式的參數」**(p.8)。
- 用途說明是**「基於合約和參數,簡要闡明一下程式的用途,它是程式要完成的任務的簡短注釋。對於大多數程式,一到兩行就足夠了,更大的程式則需要更多的資訊來說明其目的」**(p.9)。
- 書給的兩個找輸入的線索(p.9):
- 「如果問題表述包含了數學公式,公式中不同變數的數目可能就是函數的輸入數。」
- 「如果給定的是一個固定數值,它可能要在程式中出現,如果給定的是一個稍後需要確定的未知數,它就是一個輸入,而問題表述中的詢問(或要求)則提示了程式的名字。」
完成後長這樣(p.9):
;; area-of-ring : number number -> number
;; 計算一個半徑為 outer,洞的半徑為 inner 的環的面積
(define (area-of-ring outer inner) ...)3. 例子
目標:通過例子刻劃輸入和輸出之間的關係
任務:檢查問題表述得到例子;計算例子;如果可能的話檢查計算結果;構造例子(圖 2.2,p.10)
「為了更好地了解程式要計算什麼,需要構造一些輸入並確定輸出到底為何」,然後把例子寫進用途說明裡(p.9):
;; 例子: (area-of-ring 5 3) 的結果為 50.24書列了三個「為什麼要在寫程式體之前先寫例子」的理由(p.9):
- 「它是唯一可靠的在程式測試中發現邏輯錯誤的途徑。如果借助最終得到的程式來構造例子,有可能會輕信程式,因為運行程式比預測它會做什麼容易得多。」
- 「例子使我們思考資料計算過程,這對於將遇到的複雜程式體的設計是至關重要的。」
- 「例子是用途說明語句的非形式表達。」 後續的讀者——教師、同事、程式購買者——「會喜歡這些抽象概念的具體說明」。
4. 主體(程式體)
目標:定義函數
任務:闡明函數是如何計算它的結果的;使用 Scheme 基本運算、其他函數和變數構造 Scheme 表達式;如果可以的話,翻譯問題描述中的數學公式(圖 2.2,p.10)
也就是**「必須將函數頭部中的『…』替換為表達式」,而這個表達式「使用 Scheme 中的基本操作和已定義或即將定義的程式,從參數計算出結果」**(p.9)。
前提條件書講得很硬:「只有理解了如何從給定的輸入計算出結果,才可能闡明程式體。」 至於怎麼寫得出來——公式題就翻譯公式,敘述題就**「細心地挖掘其中的資訊並構造相應的表達式」,而且「觀察並理解如何從特定的輸入得到輸出的例子可能對程式體的設計也會有所幫助」**(p.9)。
5. 測試
目標:發現錯誤(語法和邏輯錯誤)
任務:應用函數於例子中的輸入資料;檢查結果與預期值是否相符(圖 2.2,p.10)
做法就是把第 3 步的例子**「如同添加等式一樣」**加在定義窗口下面,按執行、看結果對不對(p.9)。
測試的能力邊界書也寫死了:「測試不能保證程式對所有可能的輸入都產生正確的輸出,因為可能的輸入數目通常是無限的。但測試可以揭示語法錯誤、運行問題以及邏輯錯誤。」(p.9)
結果不對的時候,書要你先懷疑例子本身:「有可能例子本身就是錯誤的,也有可能程式包含了邏輯錯誤,也有可能例子和程式都有錯誤。不管是何種情況,都必須再次歷經程式開發的每一步。」(p.9)
圖 2.1:一個完整走完的例子(p.10)
;; 合約: area-of-ring : number number -> number
;; 用途說明: 計算一個半徑為 outer,其中洞的半徑為 inner 的環的面積
;; 例子: (area-of-ring 5 3) 的計算結果為 50.24
;; 定義: [函數頭部的精化]
(define (area-of-ring outer inner)
(- (area-of-disk outer)
(area-of-disk inner)))
;; 測試:
(area-of-ring 5 3)
;; 預期的值
50.24注意最後成品裡,合約、用途說明、例子全部以註解的形式留在程式碼裡,測試與預期值也留著——每一步的產物一個都沒有丟掉。
🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意
- 訣竅不會幫你想出答案。 書講得毫不客氣:「設計訣竅並不是核彈」、「它提供的是完成程式設計過程中不可避免的步驟的指導」;而且**「在程式設計中最富有創新性和最困難的一步是程式體的設計」**(p.10)。訣竅整理的是流程,不是靈感。
- 卡住的原因常常不在程式設計,而在領域。 程式體的設計**「依賴於我們閱讀和理解書面材料的能力,依賴於我們獲取數學關係的能力,依賴於我們所掌握的基本事實」(p.10)。書把這種知識稱為領域知識**:它**「可能來自簡單或複雜的數學,如算術和微分方程,或來自非數學學科,如音樂、生物學、市政工程和藝術等」(p.10)。程式設計者不可能懂所有領域,但「必須準備著去了解不同應用領域的語言,以便和領域專家溝通」**(p.10)。
- 步驟感覺繁瑣,但對「不熟的問題」最省時;熟練後會內化成直覺。(個人註記,非書中原文)
🔗 相關工具
這張卡是訣竅家族的基本款。 前言圖 0.1 的六步驟是全書骨幹,第 2 章的圖 2.2 是它在「輸入是數、輸出是數」情況下的入門版;書之後每遇到一種新的資料或問題型態,就在這個骨幹上把某一步展開、或再加一步。要處理下列情況時,改用對應的加強版:
- 工具-資料驅動的模板推導 —— 輸入是複合/自引用/相互引用資料時,「主體」前面要多一步從資料定義推出模板。書第 16 章示範相互引用的 dir 與 LOFD 時就說:「要設計一個處理 dir 的函數,我們必須並行地開發 dir 處理函數和 LOFD 處理函數的模板」(p.134)。
- 工具-從相似定義提煉抽象 —— 手上出現兩個長得很像的定義,要把它們合成一個時。
- 工具-生成遞迴與終止論證 —— 遞迴不照資料結構走時,比結構遞迴多出一步終止論證。
- 工具-累積器與累積器不變式 —— 遞迴過程中「丟失知識」時,用累積器把知識帶下去(那是做完訣竅之後才加的一步)。
- 工具-狀態變數的設計訣竅 —— 程式需要記住過去發生過什麼時。
- 工具-逐步求精 —— 反覆精化:處理的不是單一函式,而是複雜資訊的資料表示法該怎麼一步步做出來。
- 工具-偽代碼編程流程 —— 姊妹流程,都是「先寫描述再寫程式」,PPP 更偏實務常式的寫法。
- 回連 程式設計方法