探索 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 則介於兩者之間,falsenil 均為 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(其餘),但趨勢是改用更易讀的 firstrest 命名。Clojure 進一步簡化為 firstnextnext 於單元素列表回傳 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 的編輯器整合,核心哲學始終如一:程式碼即資料,語言是程式設計師的畫布。

Sources