Matthias Felleisen 等:how To Design Programs——an Introduction To Computing And Programming@2003 (第1版)

根據提供的資料,《How to Design Programs》一書的核心論點及其詳盡解釋如下:

核心論點 1:程式設計是一個應普及的「設計」過程,而非僅僅是編程語言的學習。

該書的核心主張是,電腦科學,特別是程式設計,應成為通識教育的核心部分。作者認為,與其他程式設計入門書籍不同,本書強調的是「程式設計過程」(program design process)。這不僅僅是學習如何寫出符合語法的程式碼,而是培養一種系統性解決問題的思維方式。這個過程包含了一系列重要的技能:批判性閱讀、分析性思考、創造性綜合以及對細節的關注。這些技能不僅對未來的電腦程式設計師至關重要,對任何領域的人士都同樣有益。書中認為,程式設計就像學習數學和英語一樣, teaches analytical skills and the ability to articulate thoughts precisely. 透過程式設計,學生學會分析問題陳述(通常是文字問題)、提煉核心目標、構思範例、根據分析結果擬定解決方案的輪廓、完成程式以及進行測試。每個步驟都會產生一個明確定義的中間產物,這使得整個設計過程有條不紊、易於追蹤和檢查。

核心論點 2:透過「設計食譜」(Design Recipes)提供一套明確、逐步的程式設計指南。

為了讓初學者能夠系統地學習程式設計,本書引入了一套具體的設計指南,稱之為「設計食譜」。這與傳統教學中模糊的建議(如「由上而下設計」)不同。設計食譜將程式設計過程分解為幾個明確的階段和步驟:
1. 問題分析與資料定義 (Problem Analysis & Data Definition): 分析問題描述,確定程式需要處理哪些資訊,並如何將這些資訊表示為資料。這包括定義資料的類別,特別是處理複合資料(structures)和混合資料(unions of types)。
2. 契約、目的陳述與函式標頭 (Contract, Purpose Statement, Header): 確定函式的名稱、它接收什麼類型的輸入資料、產生什麼類型的輸出資料(契約),描述函式的功能(目的),並寫出函式的定義開頭(標頭),包括函式名稱和參數名稱。
3. 範例 (Examples): 針對特定的輸入資料,手動計算出預期的輸出結果。這有助於更清楚地理解問題的要求,並在後續階段用作驗證程式正確性的測試。對於條件式函式和遞歸函式,需要確保涵蓋所有情況和邊界條件。
4. 函式模板 (Function Template): 根據輸入資料的結構(structure),自動推導出函式主體的初步框架。這是資料導向設計的核心思想。例如,處理結構體時,模板包含所有選擇器;處理列表時,模板包含對空列表的檢查和對構造列表的遞歸呼叫;處理混合資料時,模板包含一個 cond 語句來區分不同的資料類別。
5. 函式定義 (Function Definition): 在模板的基礎上,填入具體的邏輯和計算表達式來完成函式主體。這涉及將問題的解決邏輯轉化為程式碼,利用參數、選擇器、輔助函式以及控制結構(如 cond)。
6. 測試 (Tests): 運行之前建立的範例,檢查程式的實際輸出是否與預期輸出一致。這個階段旨在發現語法錯誤、運行時錯誤和邏輯錯誤。

設計食譜提供了一套可重複的流程,幫助初學者避免無從下手的困境。教師也可以利用食譜來診斷學生的問題,因為每個階段的產物都是具體的、可檢查的。

核心論點 3:資料的結構決定程式的結構(資料導向設計)。

本書強烈主張「資料導向的程式設計」(Data-Driven Program Design)。這意味著函式或程式的結構(template)應直接從其主要輸入資料的定義中推導出來。
* 如果函式處理基本原子資料(數字、符號),其結構可能相對簡單。
* 如果函式處理複合資料(結構體),函式的模板應包含對結構體中所有欄位的訪問(使用選擇器)。
* 如果函式處理混合資料(幾種類型資料的聯集),函式的主體應包含 cond 語句,根據輸入資料屬於哪種類型來分情況處理。
* 如果函式處理遞歸定義的資料(如列表或樹),函式的主體應包含一個 cond 語句,區分基本情況(如空列表)和遞歸情況(如構造列表),並在遞歸情況下,包含對資料遞歸部分的遞歸呼叫。
* 如果函式處理多個複雜資料,模板需要考慮這些資料結構的組合。

這種方法使得程式結構清晰且易於理解和修改。當資料定義發生變化時,可以直接對應地修改依賴該資料定義的函式模板和定義。

核心論點 4:程式應由輔助函式組成,並透過抽象來減少重複。

程式不應是一個龐大單一的區塊,而應由許多小型、定義清晰的函式組成。每個輔助函式都應對應於問題描述中的一個概念或依賴關係。這提高了程式的可讀性、可維護性和可測試性。當程式碼出現重複模式時,應透過「函式抽象」(Function Abstraction)將其提煉為一個更通用的函式。這通常涉及將重複的常數或操作替換為參數,甚至將函式本身作為參數傳遞(高階函式)。抽象後的函式(如 map, filter, fold)可以應用於多種類型的資料和操作,實現程式碼的複用,並將相關的功能或邏輯集中在一個地方(單一控制點)。

核心論點 5:認識並運用不同形式的遞歸(結構性遞歸與生成性遞歸)。

結構性遞歸(structural recursion)是處理遞歸定義資料時自然產生的遞歸形式,遞歸呼叫的輸入是原輸入結構上的子部分。生成性遞歸(generative recursion)則是一種更通用的遞歸形式,用於設計解決問題的演算法。在生成性遞歸中,遞歸呼叫的輸入不是原輸入的結構性子部分,而是透過某種演算法邏輯「生成」的新問題實例。這種遞歸通常需要識別問題的「可瑣碎解決」情況(trivially solvable cases),以及如何將複雜問題「生成」為更小的、可透過遞歸解決的子問題。設計生成性遞歸演算法需要額外的步驟,包括證明演算法的終止性(termination),以避免無限迴圈。快速排序(Quicksort)是生成性遞歸的經典範例。

核心論點 6:使用累加器(Accumulators)來累積計算過程中的知識。

在某些情況下,特別是在遞歸函式中,直接的結構性遞歸計算可能會丟失中間過程的有用資訊,導致效率低下或設計困難。透過在輔助函式中引入額外的參數(累加器),可以攜帶計算過程中累積的狀態或知識。累加器模式(accumulator-style)通常用於優化性能或解決某些特定問題。理解累加器的不變性(accumulator invariant)——即在遞歸呼叫之間,累加器如何保持與函式參數之間的特定關係——是設計累加器函式的關鍵。

核心論點 7:理解並控制副作用(Effects),特別是變數的狀態變化。

到目前為止,許多函式的設計都集中在根據輸入產生輸出值,而不改變任何程式的全局狀態。但現實世界的程式常常需要「記憶」過去的互動或狀態。這需要使用「狀態變數」(state variables),並透過賦值操作(set!)來改變其值。狀態變化被視為一種副作用(effect)。管理帶有副作用的程式需要仔細設計,明確函式的副作用,並使用封裝(encapsulation)將狀態變數及其相關操作隱藏起來,以提供一個受控的介面。結構體和向量的變動操作(mutators)是另一種形式的狀態變化,它們直接修改資料結構的內容。理解狀態變化對程式執行順序和變數意義的影響至關重要。

核心論點 8:透過封裝(Encapsulation)管理狀態變數和提供服務介面。

當程式需要管理多個獨立的狀態實例時(例如同時控制多個交通燈或管理多個通訊錄),簡單的全局狀態變數不足以應對。將狀態變數和所有操作該狀態變數的函式打包在一個 local 定義中,並由外部函式返回一個管理器函式(service manager),這種模式稱為封裝。封裝建立了一個具有特定服務介面的「物件」,每個透過外部函式創建的物件都擁有自己的獨立狀態變數。這允許創建任意數量的此類物件,每個物件都根據其接收到的訊息執行相應的操作並改變其內部狀態。這是建構大型、複雜、具有互動性程式的基礎。

總結來說,《How to Design Programs》透過強調設計過程、提供具體方法(設計食譜)、基於資料結構思考程式結構、鼓勵函式組合與抽象、介紹不同遞歸形式(結構性與生成性)、運用累加器模式、以及理解並控制狀態變化與副作用,為初學者提供了一套全面且系統的程式設計教育框架,旨在培養具備強大問題解決能力的思考者,而不僅僅是會寫程式的編碼員。