探索 Lisp 版圖:Common Lisp、Racket、Clojure 與 Emacs Lisp 的比較分析
Lisp 語系的語言常被視為一團充滿括號的巨石,然而在表層之下卻隱藏著多樣化的方言生態系,每一種都針對特定環境量身打造——從可擴充的 Emacs 核心到基於 JVM 的 Clojure 並發模型。對於在這些語言之間切換的開發者而言,挑戰往往不在於核心邏輯,而是語法、標準函式庫與執行模型的細微差異。
執行模型與工具鏈
雖然四種方言皆提供 Read‑Eval‑Print Loop(REPL),但它們走向正式環境的路徑差異甚大。
- Common Lisp (SBCL): 主要聚焦於編譯成機器碼。正如社群貢獻者所指出,SBCL 常在 REPL 中預設編譯程式碼,模糊了解釋與編譯的界線。
- Racket: 提供完善的模組系統與
raco工具組,用於編譯與套件管理。其獨特之處在於能透過#lang宣告輕鬆產生獨立執行檔。 - Clojure: 深度整合於 Java Virtual Machine(JVM)。其執行與 Java 生態系緊密相連,使用
.jar檔案以及 JVM 的記憶體管理與執行緒模型。 - Emacs Lisp: 主要設計為 Emacs 編輯器的擴充語言。雖支援位元組編譯以提升效能,但其主要存在形態仍是編輯器自身的執行環境。
核心語法差異
真與偽
對新手而言,最令人吃驚的差異之一是「truthy」與「falsy」值的定義。於 Common Lisp 與 Emacs Lisp 中,nil 與空列表 () 同義且皆視為 false。Racket 採取較嚴格的規則,只有 #f(或 false)為 false;null 與 () 被視為 true。Clojure 則介於兩者之間,false 與 nil 均為 falsy,但空列表 () 為 truthy。
變數作用域與賦值
變數宣告大致遵循 let 用於區域、def/define 用於全域的模式,但細節相當重要:
- Lisp-1 vs Lisp-2: Common Lisp 與 Emacs Lisp 為「Lisp-2」,意即函式與變數擁有獨立的命名空間。符號可同時對應一個值與一個函式。Clojure 與 Racket 為「Lisp-1」,符號在給定環境中只指向單一實體。
- Dynamic vs Lexical Scope: Emacs Lisp 歷史上依賴動態作用域。雖然引入
lexical-let以提供詞法作用域,但舊版的預設行為仍與 Clojure、Racket 所採用的嚴格詞法作用域不同。
資料結構細節
列表的演變
雖然 cons 單元是 Lisp 的祖傳核心,現代方言已各自演化:
- Clojure 的永續資料結構: 與 Common Lisp 或 Racket 的鏈結列表不同,Clojure 使用永續、不可變的資料結構。這使得
cons的行為有所不同;第二個參數必須是列表,且原始列表保持不變。 car/cdr的傳統: 大多數方言仍支援car(首項)與cdr(其餘),但趨勢是改用更易讀的first與rest命名。Clojure 進一步簡化為first與next(next於單元素列表回傳nil,而rest則回傳空序列)。
陣列與字典
固定長度的陣列(向量)隨處可見,但其可變性各有差異。Racket 區分不可變向量(#())與透過 (vector …) 建立的可變向量。Clojure 則大量使用不可變的映射與向量,提供跨不同序列類型的一致 API。
進階語言特性
巨集與衛生性
巨集是 Lisp 的「殺手鐧」特性,使語言得以延伸。然而安全性的處理方式各異:
- 衛生巨集: Racket 是衛生巨集的金標準,確保巨集展開不會意外捕獲外部作用域的變數。
- 非衛生巨集: Common Lisp 與 Emacs Lisp 的巨集屬於非衛生。為避免名稱衝突,開發者必須手動使用
gensym產生唯一符號。
例外處理與重啟機制
Common Lisp 提供強大的「條件系統」,將錯誤偵測與解決分離。restart-case 允許程式設計師定義多種錯誤復原方式,處理器可在不展開堆疊的情況下呼叫,這項功能在其他三種方言中基本缺乏。
總結比較表
| 特性 | Common Lisp | Racket | Clojure | Emacs Lisp |
|---|---|---|---|---|
| 命名空間 | Lisp-2 | Lisp-1 | Lisp-1 | Lisp-2 |
| 偽值 | nil, () |
#f |
false, nil |
nil, () |
| 巨集 | Non-hygienic | Hygienic | Semi-hygienic | Non-hygienic |
| 主要目標 | 本機/機器碼 | 本機/位元組碼 | JVM | Emacs 編輯器 |
| 預設作用域 | 詞法 | 詞法 | 詞法 | 動態/詞法 |
最終見解
Lisp 家族的碎片化常被視為採用的障礙,但正如某位社群成員所言,寫 Lisp 的體驗彷彿「閱讀詩歌」,因其概念優美且流暢。無論你需要 Common Lisp 的工業級編譯、Racket 的學術嚴謹、Clojure 的 JVM 互通,或是 Emacs Lisp 的編輯器整合,核心哲學始終如一:程式碼即資料,語言是程式設計師的畫布。