本文涵蓋 Lisp 語言的語法與執行方式,包括變數與表達式、算術與邏輯運算、字串、正規表達式、日期時間、串列、定長陣列、字典、使用者定義類型、函數、執行控制、例外處理、資料流、Emacs 緩衝區、檔案、目錄、程序與環境、函式庫與命名空間、物件、Lisp 巨集、反射以及 Java 互操作等主題。

以 Racket 為例,編譯 a.ss 會產生位元組碼檔案 a_ss.zo,執行時會優先使用此檔案。若要將程式編譯成獨立執行檔,必須將程式包裝成模組,並在檔案頂端加入 #lang scheme。模組中定義的所有函數會依序執行。

在檔案中如何解讀程式碼,以及如何讓腳本由直譯器自動執行,需將路徑替換為系統中直譯器的路徑。在 Emacs 中執行 Lisp 程式碼,可使用 M-x load 或 M-x load-file,前者會依 load-path 中的字串列表尋找檔案,且不必指定 .el 或 .elc 副檔名。

Emacs Lisp 中可用 shebang 實作 cat 指令。命令列可呼叫 REPL,Racket 也提供 GUI REPL 環境 DrRacket。Clojure REPL 會將每次評估結果存入 *1、*2 等變數,最後的例外存入 *e。

命令列可傳入程式執行。S 表達式的運算子與資料間以空白分隔。除 Clojure 外,Lisp 中符號可包含任意字元,特殊字元需跳脫。讀取器會將以數字開頭的字元序列視為數字,故需跳脫以建立此類符號。

Common Lisp 不區分大小寫,讀取器會將字母轉為大寫。由句點組成的符號必須跳脫。以星號開頭與結尾的符號可能與系統特殊變數衝突。符號開頭不可為 #,單一句點符號需跳脫。

Common Lisp 與 Scheme 支援多行註解,使用 #| |# 包圍。可用平衡括號註解程式碼。

Lisp 中的識別字稱為符號。nil 與空串列 '() 在大多數 Lisp 方言中相同,但有些方言區分。可測試值是否為 null。

識別字是第一類值,可存入變數。當作參數時,Lisp 直譯器會將識別字視為對應變數的值,除非使用特殊語法。

非參照識別字(值即為自身)稱為關鍵字,在 Prolog 稱為原子,在 Ruby 稱為符號。使用關鍵字作為字典鍵方便,因不需跳脫。

字串是非參照識別字的替代方案,部分語言支援字串內部化,使字串使用效率與非參照識別字相當。

Common Lisp 中,每個識別字有屬性列表(property list)。Clojure 的元資料與值綁定,為不可變物件。

Lisp-1 只在環境中為符號存一個實體,Lisp-2 可存多個,符號解析依用法決定。Common Lisp 與 Emacs Lisp 為 Lisp-2。

關鍵字為預定義符號,評估結果為其列印表示。Scheme 中使用關鍵字作為表達式會錯誤。

atom 是判斷是否為原子(非 cons cell)的謂詞。Scheme 無 atom,但有 cons?,為其邏輯否定。Clojure 無 cons cell,故 atom 若實作永遠回傳真。

所有 Lisp 有單引號巨集縮寫 quote。eval 是 quote 的單向反函數。

McCarthy 1960 年論文中,eq 在任一參數非原子時未定義。Common Lisp 與 Scheme 中 eq? 判斷兩參數是否指向同一記憶體位置。equal 與 equal? 判斷結構相等。

Common Lisp 與 Scheme 中 = 僅用於數字。Clojure 以 = 替代 eq 與 equal。

car 與 cdr 為 IBM 704 指令縮寫,現多用 first 與 rest,但 car 與 cdr 簡短且有方便的巢狀呼叫縮寫。

不同 Lisp 方言中 car、cdr 與其組合的定義略有差異。cdr 與 rest 在空串列上行為不同,Clojure 中空串列與 nil 在布林上下文中評價不同。

Common Lisp 與 Racket 支援任意精度整數,Racket 提供 fixnum 庫存取硬體整數,溢出會報錯。

Common Lisp 與 Racket 支援任意精度有理數,Racket 在某些情況使用有理數,Common Lisp 則用硬體浮點數。

Racket 提供 flonum 庫存取硬體浮點數。

Common Lisp 複數的實部與虛部類型相同,可為整數、有理數或浮點數。

在布林上下文中評價為假的值。

null 與空串列在布林上下文中不評價為假。

比較運算子與返回最小最大值的函數。

realp 與 real? 判斷數字是否為實數(虛部為零),floatp 與 inexact? 判斷是否為浮點數。

整數除法的結果集合為有理數,Common Lisp、Scheme 與 Clojure 均符合此數學定義。Emacs Lisp 執行整數除法,結果仍為整數。

Lisp 中 + 與 * 可接受零或多個參數,- 與 / 接受一個或多個參數。零參數時 + 與 * 回傳加法與乘法單位元 0 與 1。單參數時 + 與 * 回傳該參數,- 與 / 回傳加法與乘法逆元。多參數時 - 與 / 從左至右計算。

單元除法(計算倒數)會產生參數數量錯誤。

round、floor、ceiling 回傳值類型依參數而定,整數或浮點數。

inexact->exact 可將 round、ceiling、floor 回傳的浮點數轉為整數。

Math/round 回傳整數,對有理數呼叫會報錯。Math/floor 與 Math/ceil 可用於有理數,但回傳浮點數。

round、ceiling、floor 回傳整數,fround、fceiling、ffloor 回傳浮點數。

(sqrt -2) 的值。Common Lisp 與 Scheme 支援複數,Clojure 與 Emacs Lisp 不支援。

絕對值回傳值類型與參數相同。

使用 scheme/math 庫可使用 sgn 函數。

Math/signum 僅對浮點數運作,且回傳浮點數。

如何產生均勻分布與常態分布的隨機整數與浮點數。

Gambit 與 Racket 實作的位元運算子遵循已撤銷的 SRFI 33 標準。

ash 函數在兩參數皆為負時結果不同。

字元字面值語法,範例以字母 "a" 展示所有 ASCII 可列印字元的字面值寫法。

字元類型為 standard-char,判斷函數為 characterp。

字元類型為 java.lang.Character。

字元可視為整數並可用算術運算子操作,characterp 與 integerp 同義。

字串字面值中可用的跳脫序列列表。

\x 跳脫序列後可接一至六個十六進位數字,結尾可用反斜線與空白標示,例如字串 "λ123"。

如何取得字串中特定位置的字元。

尋找子字串在字串中的位置。

完整的字串比較函數列表。

上述函數也有不區分大小寫版本。

Emacs 僅有區分大小寫的字串比較函數。

string= 與 string-equal 同義,string< 與 string-lessp 同義。

如何將字串轉為數字,數字轉為字串。

read-from-string 會呼叫讀取器,回傳值不保證為浮點數。

parse-float 函數可將所有實數型別轉為浮點數,遇非實數會拋出錯誤。

string-match 回傳第一個匹配子字串的索引,找不到回傳 nil。

範例程式碼將游標移至下一個匹配位置並回傳該位置索引。

(eval '()) 等於 '() 的優點是空串列不必加引號。

nth 與 list-ref 從零開始計數,nth 超出範圍回傳 nil,list-ref 則拋錯。

如何取得串列元素的索引,第一個元素索引為零。

Clojure 中 last 與 butlast 分別類似 first 與 rest,操作串列尾端。以下程式碼對偶為恆等式。

Common Lisp 中 last 回傳含單一元素的串列,故類比失效。

以下程式碼對偶對串列執行相同操作,但不完全相同,因 rplaca 與 rplacd 回傳修改後的串列而非第二個參數。

Racket 提供 plt-r5rs 直譯器支援 R5RS 標準,DrRacket 也可設定為 R5RS 語言。

Clojure 中 assoc 回傳替換指定值的新關聯。

lambda 可接受多個參數。

mapcar 不接受多參數 lambda。

如何將關聯串列映射應用於遞迴串列。

Common Lisp 或 Emacs Lisp 中 take 的定義範例。

Racket 中使用盒子實作 push 與 pop 範例。

Clojure 中 pop 只回傳第一個元素,原串列不變。

#(1 2 3) 建立不可變向量,(vector 1 2 3) 建立可變向量。

vector-set! 對不可變向量呼叫會拋錯。

串列與向量支援相同操作,差異在操作速度。若函數名稱相同,開發者使用更便利。Clojure 最徹底,所有串列函數亦適用向量。Common Lisp 與 Emacs Lisp 部分串列函數適用向量,Scheme 則無。

回應序列函數的容器類型。

Lisp 中陣列與向量皆為固定大小集合,向量為一維陣列。僅 Common Lisp 支援多維陣列。

array-rank 回傳陣列維度數,array-dimensions 回傳各維大小,元素數為各維大小乘積。

Lisp 傳統使用成對串列作字典,本節資料結構以雜湊表實作。

除雜湊表外,還有一組函數可操作任意字典型別,Racket 中包括雜湊表、成對串列與向量。向量作字典時,值為元素,鍵為整數索引。

三種字典型別可用於本節函數。

字典字面值語法。

建構子使用 equal? 判斷鍵相等。

另有使用 eq? 或 eqv? 判斷鍵的建構子:hasheq、hasheqv、make-immutable-hasheq、make-immutable-hasheqv。

如何測試值是否為字典。

如何取得字典中鍵的數量。

如何查找鍵對應的值。

如何插入或替換鍵值對。

查找不存在鍵時的行為。

如何處理錯誤並在鍵不存在時回傳 null。

如何檢查字典中是否存在鍵。

如何移除字典中的鍵及其值。

如何將字典反轉,將原本的值映射為鍵,鍵映射為值。

若多個鍵映射至同一值,反轉時部分鍵會被捨棄。

如何遍歷字典中的鍵值對。

如何取得字典的鍵或值列表。

傳統上 let 進行平行賦值,let* 則序列賦值。

Clojure 中 let 與 let* 同義,皆序列賦值。

let 使用動態作用域,需用 lexical-let 才是詞法作用域。

Common Lisp 中命名參數為選用,可指定預設值。

呼叫時若未提供命名參數且無預設值,則設為 nil。

Scheme 中如何實作命名參數。

Emacs Lisp 中命名參數為必須,未提供會產生執行時錯誤。

ANSI Common Lisp 規範不要求實作尾端呼叫優化。

如何取得函數的文件字串。

describe 回傳包含函數簽名等資訊的文件字串,若只要文件字串可用特定函數。

如何定義帶有文件字串的函數。

如何搜尋定義與文件。

Apropos 接受模式並回傳所有匹配的符號名稱。

apropos 回傳匹配符號名稱列表。

find-doc 搜尋所有文件字串並輸出匹配項目。

apropos 與 find-doc 可接受字串或正規表達式。

apropos 在 *Apropos* 緩衝區顯示匹配定義的文件,參數為字串但視為正規表達式。

progn 及其他方言中等價函數回傳最後一個表達式的值。Common Lisp 與 Emacs Lisp 另有 prog1 與 prog2,分別回傳第一或第二個表達式的值。

呼叫 error 會引發 exn:fail 類型的例外。

condition-case 的第二個參數為可能引發錯誤的程式碼,第三個參數為錯誤處理器,處理器以 condition 指定捕捉的錯誤類型,最後的 nil 為整個 condition-case 表達式的回傳值。

錯誤無法由 catch 處理,未捕捉的 throw 會產生錯誤,可由 condition-case 錯誤處理器捕捉。

如何定義帶有負載的自訂例外。

:report 子句非必要,若定義,例外被 Lisp 除錯器處理時會顯示。

Emacs throw 表達式的第一個參數識別例外類型,第二個參數為捕捉該例外的 catch 表達式回傳值。

以下 catch 表達式會回傳 nil。

確保檔案句柄關閉的可選技巧。

printf 輸出至標準輸出,format 回傳字串。

format 語句回傳產生的字串,用於輸入輸出時會在 Emacs minibuffer 顯示。

如何從組件建立檔案路徑名稱。

如何擷取路徑名稱中的目錄部分與非目錄部分。

如何取得路徑名稱的絕對路徑,若為相對路徑會附加目前工作目錄。

如何遍歷目錄中的檔案。

如何建立目錄,包括路徑中指定的父目錄。

如何複製目錄及其內容。

如何刪除目錄及其內容。

如何測試目錄是否存在。

當 Emacs 以 shebang 模式(即 --script 選項)執行時,全域變數 command-line-args 與 argv 會被設定。command-line-args 包含用於呼叫 Emacs 的路徑及啟動時處理的選項與其他參數,argv 僅包含額外參數。

如何載入檔案並評估頂層表達式。

不顯示任何評估結果。

顯示最後一次評估結果。

macroexpand 會遞迴展開巨集直到表達式首部不再是巨集,不會展開作為參數的巨集。

Common Lisp 另有 macroexpand-1,非遞迴展開一次巨集,展開首部可能仍是巨集。

Clojure 也有 macroexpand-1,範例見上文。

Emacs 有 macroexpand-all,遞迴展開巨集直到首部與參數皆無巨集。

語言是否有巨集展開保證不引入名稱衝突。

如何取得符號所指實體的資料型別。

srfi-1 庫引入常用串列函數,Kawa 預設不提供,詳見 SRFI。

ANSI 規範、SBCL 使用手冊、Quicklisp。

套件管理使用 Quicklisp,安裝與載入 cl-ppcre 庫範例。

Quicklisp 會在使用者主目錄建立 quicklisp 目錄,下載安裝後可如此使用。

可將 Quicklisp 自動載入指令放入 .sbclrc 檔案。

Racket 參考資料:Racket 語言、PLaneT、套件。

Racket 內建大量函式庫於 collects 目錄,可用 require 指令載入,路徑為 collects 目錄相對路徑,不含 .rkt 副檔名。Racket 5.1 版本包含 50 個 SRFI 函式庫。

Racket 也有內建套件管理系統,可瀏覽可用套件列表,點選套件詳細頁取得 require 字串載入。執行 require 字串會將函式庫下載至使用者主目錄。

以 Mac 為例,PostgreSQL 資料庫綁定檔案安裝於 ~/Library/Racket。

Clojure 參考與速查表。

Java 程式碼呼叫基礎。

Clojure 自動匯入 java.lang 中所有類別。

上述語法有簡寫方式。

因為是原始型別非物件,Clojure 提供專用於 Java 陣列的函數。

GNU Emacs 手冊與 Emacs Lisp 參考手冊。

在 Emacs 內部取得 Emacs Lisp 程式設計入門。

執行 M-x lisp-interaction-mode 進入 Lisp 互動模式,該模式下 C-x e 評估當前行的 s-expression,M-x eval-buffer 評估整個緩衝區。

使用 Lisp 互動模式定義可由 Emacs 呼叫的函數,範例如定義 dired-emacs-lisp 函數瀏覽 Emacs Lisp 目錄。

目錄路徑硬編碼,系統可能不同。定義後可用 M-x dired-emacs-lisp 呼叫。非所有 Lisp 函數皆可如此呼叫,可呼叫者稱為命令。

命令主體可有文件字串,接著呼叫 interactive,再執行命令程式碼。文件字串可用 M-x describe-function 查詢。

interactive 呼叫使函數成為命令,可帶選用參數。用 M-x describe-function 查詢 interactive 參數說明。

欲將命令綁定至快捷鍵 C-c l,可於 Lisp 互動模式執行綁定指令。

若希望命令與快捷鍵於 Emacs 啟動時自動可用,將指令放入 ~/.emacs.d/init.el 檔案。