Paul Graham:on Lisp (中文版)
《On Lisp》主要論點闡述
保羅·格雷厄姆的《On Lisp》一書的核心主旨是闡釋 Common Lisp 作為一種「可程式化的程式語言」(programmable programming language)的獨特能力。這門語言的設計允許程式設計師不僅僅是使用語言來編寫程式,更能修改和擴展語言本身,使其更好地適應特定的問題域,進而實現更為優雅、高效且易於維護的軟體。這本書透過深入探討函數、宏以及在這些基礎上構建的高級抽象,全面展示了 Lisp 的這種強大潛能及其所催生的「自底向上」程式設計方法。
1. Lisp 的核心:可擴展性與自底向上設計
本書開宗明義地指出,Lisp 與其他程式語言的根本區別在於其無與倫比的可擴展性。Lisp 本身就是用 Lisp 編寫的,且 Lisp 程式的表現形式(列表)同時也是 Lisp 的數據結構。這種同像性(homoiconicity)使得 Lisp 程式可以像數據一樣被操縱,尤為重要的是,可以編寫程式來生成或轉換程式碼。這種能力使得程式設計師能夠為語言添加新的操作符,而這些操作符與內建操作符享有同等地位。
這種可擴展性直接導向「自底向上」的程式設計方法。傳統的「自頂向下」方法是先進行全面規劃,再逐步細化實現;而自底向上則鼓勵程式設計師在解決問題的過程中,同步構建一套更適合該問題領域的程式語言或抽象層。程式設計師會根據程式需求,創建新的函數或宏,將通用模式抽象化,使底層語言更貼近問題的表達方式。最終的程式碼更精煉、更接近問題本身的邏輯,因為大量通用的或領域特定的邏輯已被「推」入到底層的擴展語言中。這種方法論的優勢在於:
- 適應複雜和變化的需求: 對於需求不明或不斷演進的複雜軟體,先規劃再實施可能失效。自底向上允許邊寫程式邊設計,通過不斷迭代和重構語言與程式的邊界,使設計與實現更緊密地結合。
- 代碼重用: 開發過程中創建的通用或領域特定的抽象(函數和宏)可以在同一個專案的不同部分或不同的專案中重複使用,隨著時間推移,可以積累一套強大的工具庫。
- 提高程式碼的表達力和清晰度: 通過創建更高級的抽象操作符,程式碼可以更直接地表達其意圖,減少重複和低層次的細節,提高可讀性。
2. 基石:函數作為第一類對象
Lisp 中函數作為第一類對象是實現許多高級抽象的基礎。這意味著函數可以:
-
儲存和傳遞: 函數可以被賦值給變量,儲存在數據結構中,並作為參數傳遞給其他函數(高階函數)。例如,
mapcar、remove-if等內建函數利用這一點實現通用列表操作。程式設計師也可以創建自己的高階函數來抽象重複的行為模式。 - 作為返回值: 函數可以生成並返回新的函數(函數生成器)。結合詞法作用域(Common Lisp 的默認行為)和閉包,返回的函數可以「記住」其創建時環境中的變量綁定,從而攜帶局部狀態或根據參數定製行為,這為創建靈活且帶有狀態的抽象提供了基礎。
3. 核心工具:巨變的推手——宏
雖然函數提供了強大的抽象能力,但許多語言擴展和語法抽象必須依賴宏。宏是 Lisp 中實現程式碼轉換的工具,它們在程式被編譯或求值之前運行,接收宏調用的程式碼作為輸入,並生成新的程式碼作為輸出。這使得宏能夠改變程式碼的結構和求值方式,實現函數無法做到的能力:
-
控制參數求值: 宏可以完全控制其參數的求值方式。與函數不同,宏可以選擇不求值參數、多次求值參數,或根據條件選擇性地求值參數。這對於實現新的控制結構(如
when、unless、循環宏while、for、do的變體)至關重要。 -
注入程式碼到調用環境: 宏的展開程式碼直接插入到宏調用的位置。這使得宏可以利用或修改調用環境的詞法作用域,例如創建新的局部變量綁定(如
let的宏實現)或引用調用環境中的特定符號(如 anaphoric 宏)。 - 編譯期計算: 宏展開發生在編譯階段。如果宏的邏輯可以依賴於宏參數的結構或在編譯時已知的值,這部分計算就可以提前完成,從而生成更高效的運行時程式碼,例如在編譯期計算常量、展開循環等。
-
實現語法轉換與抽象: 宏最根本的作用是實現程式碼的語法轉換。通過宏,程式設計師可以創建新的語法結構,將複雜或重複的程式碼模式轉換為更簡潔、更具表達力的新語法,例如定義自己的
with-宏來創建特定的上下文環境,或者實現更通用的廣義變量操作符。
4. 編寫宏的挑戰與技巧
宏的強大能力伴隨著編寫時需要注意的潛在陷阱:
-
變量捕捉(Variable Capture): 宏展開生成的程式碼中的符號,可能意外地與調用環境中的符號同名,導致符號的綁定或引用不符合預期。這是宏編寫中最常見且難以偵測的錯誤之一。應使用
gensym生成唯一的符號,或小心處理宏參數和展開程式碼中的符號,以避免捕捉。雖然通常應避免變量捕捉,但有時也可被有意利用來實現特定的抽象。 - 參數求值問題: 宏展開的程式碼可能導致宏參數被求值多次或以非預期的順序求值。如果參數表達式有副作用,這可能導致程式行為異常。應確保宏展開程式碼能正確處理參數的求值次數和順序,通常通過引入臨時變量來儲存參數的求值結果。
- 非函數式的展開器: 宏展開器本身應盡量保持為純函數,其生成的展開程式碼僅依賴於宏參數,不受外部狀態或宏被展開的時間/次數影響,以確保程式碼的行為一致。
- 宏的遞歸: 直接在宏定義中進行程式碼遞歸(而非展開函數遞歸)可能導致宏展開永不終止,編譯失敗。
5. 高級抽象:從實用工具到嵌入式語言
利用函數和宏,程式設計師可以在 Lisp 中構建多個層次的抽象:
- 實用工具(Utilities): 宏常用來實現通用或領域特定的實用工具,這些工具將程式中常見的模式抽象為簡潔的操作符,提高程式碼的整潔性和可讀性。
- 宏的生成器(Macro-defining Macros): 當多個宏的定義本身呈現模式時,可以編寫宏來生成這些宏,進一步提高程式碼生成的抽象層次。
- 讀取宏(Read Macros): 在程式碼被讀取階段進行轉換,允許創建新的語法元素和數據表示形式,進一步擴展 Lisp 的語法。
- 解構(Destructuring): 作為賦值的廣義形式,允許方便地從列表、向量、結構體等數據結構中提取和綁定值到變量。通過宏,可以將解構能力擴展到任意序列或結構。
-
廣義變量(Generalized Variables): 宏可以實現「可修改的場所」(setfable places),允許使用統一的
setf語法修改數據結構的不同部分,並在此基礎上構建更豐富的操作符。 - 嵌入式語言(Embedded Languages): 這是自底向上設計的最高體現。Lisp 作為基語言,其同像性、宏能力、以及可訪問的讀取-編譯-求值循環,使其成為實現嵌入式語言的理想平台。可以將特定領域的邏輯(如數據庫查詢、解析器、邏輯程式設計)作為嵌入式語言實現,通過宏將其語法轉換為 Lisp 程式碼,並利用 Lisp 編譯器對生成的程式碼進行優化。查詢編譯器、ATN 分析器和初步的 Prolog 實現,都展示了這種方法的強大。
6. 更抽象的控制流與對象系統
本書進一步展示,通過宏和閉包,可以在語言層面實現更複雜的控制結構和程式設計範式:
- 續延(Continuations): Lisp 的詞法閉包和宏可以模擬續延,捕獲和保存程式執行狀態(包括調用棧的「未來」計算)。雖然宏實現的續延可能效率不如語言內建,但它們為實現高級控制結構提供了可行基礎。
-
非確定性(Nondeterminism): 利用續延,可以實現非確定性的選擇操作符 (
choose,fail)。這使得編寫搜索和推理算法時,可以採用更直觀、更接近問題描述的風格,而將回溯等確定性實現細節隱藏在抽象之下。ATN 和 Prolog 的實現大量依賴這種非確定性抽象。 - 面向對象編程(CLOS): Common Lisp 對象系統(CLOS)並非傳統語言中獨立的面向對象模塊,而是建立在 Lisp 核心能力(函數、閉包、分派、宏)之上的宏和函數的集合。類、實例、方法、繼承、方法組合等概念在 Lisp 中有自然的對應,展示了 Lisp 作為一種可擴展語言,可以以獨特的方式實現和整合多種程式設計範式。方法的分派(Dispatching)基於參數類型,是 CLOS 的核心機制,也是 Lisp 函數能力的一種推廣。
結論
《On Lisp》的核心論點在於展示 Lisp 的可擴展性是其根本力量所在。這種力量透過函數和宏得到釋放,使得「自底向上」的程式設計方法得以實現。通過這種方法,程式設計師可以根據特定問題的需要,創建多個層次的抽象,從基本實用工具到完整的領域特定嵌入式語言和程式設計範式(如面向對象、非確定性邏輯程式設計)。本書詳細闡述了這些抽象的構建方式、涉及的技術細節和潛在挑戰,最終證明 Lisp 是一種能夠不斷演化和適應的語言,允許程式設計師以前所未有的方式塑造他們的工具和解決方案。
comments
comments for this post are closed