Robert Chassell:an Introduction To Programming In Emacs Lisp@2001 (文档版本 2.05)

Emacs Lisp 程式設計簡介:核心概念詳解

本文件從提供的《An Introduction to Programming in Emacs Lisp》教材中提取了早期章節(主要是第 1 章至第 7 章,並觸及後續章節的關鍵概念如迴圈、正規表達式及調試簡介)所介紹的核心程式設計論點與概念,並使用繁體中文進行詳盡解釋。這些概念是理解 Emacs Lisp 乃至開始編寫簡單擴充功能程式的基礎。

程式碼與資料的統一表示:S-表達式

Emacs Lisp 的一個最根本特性是它使用列表(List)來表示程式碼和資料。這兩者都以一種稱為「符號表達式」(Symbolic Expression, 簡稱 s-expression) 的結構呈現。

  • 列表 (List): 在 Lisp 中,列表是由括號 () 包圍的元素序列。這些元素可以是原子或其他的列表。列表的元素之間使用空白(空格、Tab 或換行)分隔。例如 (+ 2 2) 是一個列表,(rose violet daisy) 也是一個列表。空列表表示為 (),它在 Lisp 中有特殊意義,也常被用來表示「假」(false)。
  • 原子 (Atom): 原子是不能再細分的 s-表達式單元。原子有不同的種類,包括:
    • 符號 (Symbol): 這是最常見的原子類型,看起來像單詞或數字與符號的組合,例如 defun+fill-columnmy-function。符號是 Lisp 中程式碼和資料的基本命名單位。
    • 數字 (Number): 例如 742-3.14
    • 字串 (String): 用雙引號 "" 包圍的文字序列,例如 "Hello, world!"。在 Lisp 中,整個字串(包括其中的空白和標點)都被視為一個不可分割的原子。
  • 空白: 在列表中,空白(空格、Tab、換行)只用於分隔元素,其數量和形式通常不影響列表本身的結構。這使得程式碼可以通過縮排來提高可讀性,Emacs 的 Lisp 模式提供了自動縮排的工具。
  • 引用 (Quoting): 單引號 ' 在 Lisp 中用於「引用」其後的 s-表達式。被引用的表達式不會被求值,只會返回其自身的值。例如,對 (+ 2 2) 求值會得到 4,而對 '(+ 2 2) 求值會得到列表 (+ 2 2)。引用是區分程式碼(待執行)和資料(待處理)的重要方式。

Lisp 解釋器與求值過程 (Evaluation)

Lisp 解釋器負責理解和執行 Lisp 代碼。當你要求解釋器對一個 s-表達式求值時,它遵循一套基本規則:

  1. 原子求值:
    • 數字和字串求值後返回其自身。
    • 符號求值時,解釋器會查找該符號當前綁定的值(如果它作為變數使用),並返回該值。如果符號沒有綁定值,會報錯(void-variable)。
  2. 列表求值: 如果列表未被引用,解釋器會將列表的第一個元素視為一個函式或特殊形式(Special Form)的名字,並對列表其餘元素(稱為引數)求值。
    • 函式求值: 對於普通函式,解釋器先對所有引數求值,然後將這些求值結果作為引數傳遞給函式定義中的指令集,執行這些指令。函式執行完成後,返回一個結果值。
    • 特殊形式求值: 特殊形式(如 defun, setq, if, let, while 等)具有特殊的求值規則,它們的引數可能不會像普通函式那樣在傳遞前被求值。特殊形式通常用於定義函式、控制流程或進行變數綁定等核心操作。
  3. 巢狀表達式: 當一個列表包含其他列表時(巢狀結構),解釋器會先對最內層的列表求值,將其結果用於求值包含它的外層列表。這個過程從內向外、從左到右進行,直到最外層表達式求值完成。求值結果會被返回。

求值一個表達式可能不僅返回一個值,還可能產生「副作用」(Side Effect),例如移動游標、修改緩衝區內容、印出訊息等。在 Emacs Lisp 中,許多編輯命令的實際作用是執行一個具有明顯副作用的函式。

函式定義 (Function Definition) 與 defun

Emacs Lisp 程式碼的核心是函式定義。函式定義是使用特殊形式 defun 來建立的。defun 定義了特定函式名稱下要執行的指令集。

defun 表達式的基本結構如下:
lisp
(defun 函式名稱 (引數列表)
"文件字串..."
(interactive 互動資訊) ; 可選
主體表達式...)

* 函式名稱: 符號,用於識別和呼叫函式。
* 引數列表: 用括號 () 包圍的符號列表。這些符號是函式的形式引數,在函式被呼叫時,實際引數的值會被「綁定」到這些符號上。這些引數符號的作用範圍僅限於函式定義內部。如果函式不需要引數,則使用空列表 ()&optional 關鍵字用於標記可選引數。
* 文件字串 (Documentation String): 描述函式用途的字串。當用戶使用 C-h f (或 M-x describe-function) 查詢函式時,會顯示這段文字。良好的文件字串對程式碼維護至關重要。
* interactive: 特殊形式,標記函式為互動式命令,可通過 M-x 或綁定的按鍵直接由使用者呼叫。interactive 後的字串指定如何從使用者那裡獲取引數(例如,"p" 表示前綴數字引數,"r" 表示區域,"b""B" 表示緩衝區名)。
* 主體 (Body): 函式定義中除上述部分之外的表達式序列。當函式被呼叫時,解釋器會按順序求值這些表達式。整個函式求值結果是主體中最後一個表達式的求值結果。progn 特殊形式可用於組合多個表達式,並返回最後一個的值。

變數 (Variable) 與 setq / let

符號除了可以作為函式名,也可以作為變數名來儲存值。這兩者是獨立的。

  • 變數設定 (set / setq): setq 是常用的特殊形式,用於設定一個或多個變數的值。setq 後跟一對或多對「變數名 值」的組合。setq 會自動引用變數名。set 類似但需要手動引用變數名,較少直接用於設定變數值。例如 (setq count 0) 將符號 count 的值設為 0。設定變數值是 Lisp 中常見的副作用。
  • 局部變數 (let): let 是另一種重要的特殊形式,用於創建作用範圍僅限於 let 表達式主體內的局部變數。這避免了變數名衝突,提高了代碼模組化程度。let 後跟一個變數列表(通常是由「變數名 初始值」組成的子列表),然後是 let 的主體。let* 類似 let,但允許後面的變數初始值引用前面已綁定的變數。局部變數的綁定在 let 表達式求值完成後自動失效。

控制流程:條件判斷 (if / cond) 與迴圈 (while)

程式需要根據條件執行不同分支或重複執行某段代碼。

  • 條件判斷 (if): if 特殊形式用於簡單的條件判斷。(if 測試表達式 then-表達式 else-表達式)。解釋器求值 測試表達式。如果結果為非 nil (即為真),則求值 then-表達式;否則求值 else-表達式 (如果存在)。else-表達式 是可選的。
  • 真與假 (Truth and Falsehood): 在 Emacs Lisp 中,只有 nil 被視為「假」(false)。其他任何值都被視為「真」(true),包括數字、字串、非 nil 的符號、列表等。符號 t 常用作表示「真」的標準值。
  • 多重條件 (cond): cond 特殊形式用於處理多個條件分支。它包含一系列「(測試表達式 結果表達式…)」對。解釋器按順序求值每個測試表達式,直到找到一個為真的。然後求值該對應的結果表達式序列,並返回最後一個結果表達式的值。如果所有測試表達式都為假,cond 返回 nil
  • 迴圈 (while): while 特殊形式用於重複執行代碼。(while 測試表達式 主體表達式...)。解釋器先求值 測試表達式。如果結果為真,則按順序求值 主體表達式,然後再次求值 測試表達式。這個過程重複進行,直到 測試表達式 求值結果為假。while 迴圈通常用於其副作用,本身求值結果總為 nil。編寫 while 迴圈時必須確保測試表達式最終會變為假,以避免無限迴圈。

Emacs 環境特定概念

Emacs Lisp 程式通常需要與 Emacs 的編輯環境互動。

  • 緩衝區 (Buffer) 與檔案 (File): 緩衝區是 Emacs 在記憶體中儲存文字的地方,你可以直接在緩衝區中編輯。檔案是儲存在磁碟上的永久性資料。通常一個緩衝區「訪問」(visit) 一個檔案,緩衝區的修改需要存檔才會寫回檔案。buffer-name 返回緩衝區名(字串),buffer-file-name 返回訪問的檔案名(完整路徑字串),current-buffer 返回當前緩衝區本身。switch-to-buffer 用於互動式切換緩衝區並顯示,set-buffer 用於程式內部切換緩衝區而不改變顯示。
  • 點 (Point) 與標記 (Mark): 點是當前游標的位置,以從緩衝區開頭算起的字元數表示。標記是用 C-hSPC 等命令設定的另一個位置。區域 (Region) 是點和標記之間的文字範圍。point 函式返回點的數值位置,point-minpoint-max 返回緩衝區(或受限區域)的起始和結束位置數值。
  • 保存編輯狀態 (save-excursion, save-restriction):
    • save-excursion:非常常見的特殊形式。它會儲存執行前點、標記和當前緩衝區的位置和狀態。求值其主體後,無論主體內的代碼如何移動點、標記或切換緩衝區,save-excursion 都會將其恢復到先前的狀態。這確保了函式的執行不會意外地改變用戶的編輯視圖。
    • save-restriction:類似 save-excursion,但用於保存和恢復「受限區域」(Narrowing) 的狀態。受限區域功能允許用戶或程式只操作緩衝區的某一部分。

列表操作基礎 (car, cdr, cons)

car, cdr, cons 是 Lisp 處理列表的基礎積木。它們的名稱來源於早期 Lisp 實現的硬體結構,現在更多地從概念上理解。

  • car (Contents of the Address part of Register): 返回列表的第一個元素。例如 (car '(a b c)) 返回 acar 不改變原列表(非破壞性)。
  • cdr (Contents of the Decrement part of Register): 返回列表除了第一個元素以外的「其餘部份」。結果總是一個列表(除非原列表只有一個元素,cdr 返回 nil)。例如 (cdr '(a b c)) 返回列表 (b c)cdr 也不改變原列表(非破壞性)。
  • cons (Construct): 構造列表。接受兩個引數,將第一個引數添加到第二個引數(必須是列表)的「前面」,形成一個新的列表。例如 (cons 'a '(b c)) 返回列表 (a b c)cons 是建立列表的基本方式。
  • 破壞性操作 (setcar, setcdr):
    • setcar: 改變列表中第一個元素的值。例如 (setq l '(a b c)) (setcar l 'x) 會使 l 變為 (x b c)。這是一個破壞性操作,直接修改了原列表結構。
    • setcdr: 改變列表中除了第一個元素以外的「其餘部份」。例如 (setq l '(a b c)) (setcdr l '(y z)) 會使 l 變為 (a y z)。這也是一個破壞性操作。

透過這些基本概念,可以開始理解和編寫更複雜的 Emacs Lisp 程式,例如處理文字區域、進行正規表達式搜尋、處理殺戮環 (kill ring) 等,這些在後續章節中被詳細介紹。