Emacs/Emacs Lisp 入門
Emacs Lisp 是一種程式語言,屬於 Lisp 語言家族(包括 Scheme 和 Common Lisp)。Lisp 是目前仍在使用的第二古老的程式語言(僅次於 Fortran),但儘管 Lisp 社群仍然活躍,但現在規模非常小。因此,大多數開發人員從未有理由學習 Lisp,並且許多使用 Emacs 的開發人員將 Emacs Lisp 視為外星領地。
儘管 Emacs Lisp 不是主流程式語言,但它是一種強大的語言,用於實現 Emacs 本身的大部分功能。這意味著作為 Emacs 擴充套件作者,您使用的是與最初用於編寫編輯器的相同的語言,並可以訪問所有相同的庫。這使得 Emacs 具有獨特的強大功能;雖然其他編輯器(如 Eclipse)接受修改其行為的擴充套件,但沒有其他編輯器可以在執行時使用原生程式碼調整其行為。
Emacs Lisp 是 Emacs 思維方式中如此基礎的一部分,以至於它不會讓您去某個特殊的地方執行 Lisp 表示式;您可以在任何緩衝區中執行它。在編寫 C 函式的過程中,您可以直接在其中插入一些 Emacs Lisp,執行它,並立即看到結果。
進入任何 Emacs 緩衝區並鍵入以下內容
(+ 1 2)
將游標定位在右括號之後,然後鍵入C-x C-e(按住 Ctrl 鍵並按 x,然後按住 Ctrl 鍵並按 e)。表示式將被求值,結果將顯示在迷你緩衝區中。此表示式只是將值 1 和 2 相加,因此結果值 3 應該出現在迷你緩衝區中。
雖然計算很簡單,但它的表達方式可能會讓一些人感到困惑。Emacs Lisp 中的表示式始終採用相同的形式:左括號、函式識別符號、該函式的引數列表,最後是右括號。上面所有的表示式意味著我們正在呼叫+函式(加法)併為其提供 1 和 2 的引數。
這種寫操作的方式被稱為波蘭表示法(也稱為波蘭字首表示法或簡稱字首表示法)。對於大多數程式設計師來說,看到加法以這種方式編寫一開始會覺得不自然,因為他們習慣於看到用運算子位於中間的數學表示式,即所謂的中綴表示法。但是,大多數程式設計師也熟悉呼叫函式,其中函式名稱出現在引數之前。大多數程式語言都區分運算子(使用中綴表示法)和函式(使用字首表示法)。在 Lisp 家族的語言中,沒有這種區別,所有呼叫都使用字首表示法。雖然這需要一些時間來適應,但它帶來的好處是所有程式碼都遵循相同的結構,這使得 Lisp 程式碼更容易閱讀和編寫 Lisp 程式碼,從而使語言更擅長自省。
如果這讓你感到困擾,值得提醒自己函式和運算子之間的分界線是多麼隨意。特別是在允許重新定義或專門用於類的面嚮物件語言中,它們實際上只是對函式呼叫的語法糖。
函式呼叫中可以有任意數量的引數(假設函式支援它,大多數函式在這樣做有意義的情況下都會支援)。
(+ 1 2 3 4)
函式的引數本身可以是函式呼叫。例如,以下表達式將前三個平方數相加
(+ (* 1 1) (* 2 2) (* 3 3))
左括號後的第一個元素必須始終是函式識別符號。
括號是表示式不可分割的一部分;如果從(+ 1 2)中刪除括號,則那裡沒有 Lisp 表示式(準確地說,有三個單獨且不相關的 Lisp 表示式,沒有函式求值)。額外的括號並不像在某些語言中那樣無害。考慮表示式
((+ 1 2 3))
表示式是遞迴求值的,因此內部表示式首先被求值,這將簡化為
(6)
此表示式的含義是應用沒有引數的函式6,但由於沒有這樣的函式,因此會丟擲錯誤。您可以將括號視為類似於 C 中函式呼叫的括號,而不是像簡單的算術括號一樣用於修改運算子優先順序。
事實上,Lisp 運算子表示法的優點之一是運算子的求值方式永遠不會有任何歧義,因此永遠不需要額外的括號來消除歧義。例如,使用中綴表示法,表示式5 * 4 + 3是可能的,這需要讀者瞭解優先順序規則才能知道它將如何被求值((5 * 4) + 3或5 * (4 + 3))。在 Lisp 語言中,等效的表示式將被寫成(+(* 5 4) 3),因此永遠不會有任何歧義。
雖然您可以從 Emacs 中的任何緩衝區執行 Emacs Lisp 語句,但最方便的做法是為該目的保留一個緩衝區,以避免弄亂包含重要工作的緩衝區。當您啟動 Emacs 時,它會為您建立一個特殊的緩衝區*scratch*,該緩衝區與檔案無關(除非您稍後決定儲存其內容)。這使得它成為編寫和執行 Emacs Lisp 語句的良好選擇。
*scratch*緩衝區對於 Lisp 執行還有另一個優勢,即預設情況下,它以 Lisp 互動模式啟動。在此模式下,您可以使用C-j執行任何 Emacs Lisp 表示式,結果將永久插入緩衝區中,而不是暫時顯示在迷你緩衝區中。您可以使用M-x lisp-interaction-mode將任何緩衝區置於 Lisp 互動模式。
正如您所料,您可以在 Emacs Lisp 中定義自己的函式,它們的工作方式與內建函式相同。您可以使用defun表示式定義函式
(defun my-add (x y)
(+ x y))
嘗試將此內容鍵入 emacs 並求值該表示式。Emacs 應該回復
my-add
定義函式的返回值只是函式本身,在本例中為my-add。這說明了一個重要觀點:每個 Lisp 表示式都有一個值。這類似於 C 中函式的返回值,只是您不必顯式返回任何內容。Emacs 只會獲取函式體中的最後一個表示式並將其視為返回值。Emacs Lisp 中沒有void函式,儘管呼叫者可以隨意忽略任何不感興趣的返回值。
定義了新函式後,您現在就可以像使用內建函式一樣使用它了
(my-add 1 2)
這給出了預期的結果3。請注意,您不能像使用內建函式(+)一樣,向此加法函式傳遞任意數量的引數。別擔心,建立可以以這種方式工作的函式是完全可能的,我們稍後將學習如何做到這一點。
任何 Lisp 中最簡單的的資料結構是列表;實際上,列表賦予了 Lisp 程式語言其名稱,它是列表處理的縮寫。這個名字也許有點用詞不當,因為 Lisp 可以用於比列表豐富得多的資料結構,但無處不在的列表為探索 Lisp 程式設計提供了自然的起點。
將以下內容鍵入 Lisp 求值緩衝區並執行它(如果您處於 lisp 互動模式,則為C-j,否則為C-x C-e)
(list 1 2 3)
Emacs 將回復
(1 2 3)
列表可以包含任意數量的專案(或零個專案),並且這些專案不必都具有相同的型別。事實上,列表可以包含其他列表作為條目,形成巢狀的資料結構
(list 1 2 "buckle my shoe" (list 3 4))
如果您對它進行求值,Emacs 將回復
(1 2 "buckle my shoe" (3 4))
當您輸入表示式時,Emacs 會對其進行求值並將結果返回給您。表示式(list 1 2 3)使用三個引數對list函式進行求值。呼叫list函式的結果是一個列表,它將被返回給您:(1 2 3)。那麼,如果(1 2 3)是列表的正確語法,為什麼您必須呼叫list函式,而不是直接鍵入列表呢?嘗試在 Emacs 中執行以下 Lisp 表示式
(1 2 3)
Emacs 將顯示錯誤訊息[1]
Lisp error: (invalid-function 1)
這裡的問題是,透過輸入列表並要求 Emacs 對其進行求值,您要求將其視為 Lisp 程式碼,而不是資料。Lisp 程式碼由一個或多個專案列表組成,其中每個列表中的第一個專案必須是函式識別符號,列表中其餘的專案是函式的引數(它們本身可以是需要求值的表示式)。由於您嘗試求值表示式(1 2 3),因此 Emacs 假設1必須是函式識別符號,當它找不到這樣的函式時,它會丟擲一個錯誤。(list 1 2 3)不會出現同樣的問題,因為list是一個函式;這是程式碼,而不是資料。
上面一段話中有一個重要的點需要重複強調:Lisp 程式碼僅僅是 Lisp 資料,所有 Lisp 資料都可以被視為程式碼。這也許是 Lisp 與所有其他主流程式語言的關鍵區別。它使得 Lisp 程式碼在執行時生成更多 Lisp 程式碼變得相對容易。這個簡單的設計決策一舉使語言的豐富性呈指數級增長,因為在其他語言中需要特殊語言支援的高階功能,只需使用普通的 Lisp 程式碼即可編寫。
如果 Lisp 程式碼和資料之間唯一的區別在於程式碼會被求值而資料不會,那麼我們必須有一種方法來告訴 Lisp 特定的資料是否需要被求值。正如我們上面看到的,一種方法是使用像 list 這樣的簡單函式,它只是返回它的引數。這是一種解決問題的方法,但不是一種優雅的方法。透過使用 list,我們並沒有成功地阻止求值發生,我們只是寫了一個求值很簡單的表示式。更嚴重的是,對於巢狀列表,您必須修改每個巢狀列表的級別(例如 (list 1 2 (list 3 4)),而不僅僅是最外層)。
由於 Lisp 的預設行為是求值列表,因此我們只需要某種方法來阻止求值即可控制求值。這是透過 quote 運算子完成的,它像任何其他 Lisp 函式呼叫一樣編寫,但會導致其引數不被求值。quote 只接受一個引數,但引數可以是一個列表,當然也包括巢狀的子列表。嘗試評估以下內容
(quote (1 2))
(quote (1 (2 3) 4))
(quote 1 2)
第三種形式會丟擲一個錯誤,因為 quote 只允許一個引數。第二種形式表明子表示式 (1 2) 沒有被求值,即使正常的求值是依次求值每個引數。
巢狀表示式的行為表明了一些重要的事情:quote 不是一個普通的 Lisp 函式。如果您嘗試自己實現 quote,您會發現這是不可能的,因為 quote 的引數會在您的自定義函式被呼叫之前就被求值。由於錯誤(在上面的例子中求值 (1 2))會在您的函式被呼叫之前丟擲,因此您的程式碼無法從中恢復。
quote 不是 Lisp 函式,而是一小部分特殊形式之一。特殊形式與 Lisp 函式具有相同的語法,但具有 Lisp 直譯器提供的普通函式無法提供的特殊行為。
您可能想知道為什麼可以求值某些表示式而不能求值其他表示式。例如,以下所有表示式都可以被求值,即使它們都不包含函式
12
"twelve"
nil
:thing
即使這些表示式都沒有被引用,它們也都求值為自身。這是因為 Lisp 將某些值視為自求值,這意味著如果將它們作為 Lisp 表示式求值,它們將返回它們自己的值。這適用於整數和字串,並且通常在沒有歧義的情況下適用。它不適用於列表,因為普通程式碼是用列表的語法表達的,因此如果列表求值為自身,則無法求值任何程式碼。
由於 quote 預計會被頻繁使用,因此有一個方便的語法糖 - 一個撇號字元 '
(quote (1 2))
'(1 2)
這兩個表示式都求值為相同的 (1 2) 列表。
| 本節內容是一個存根。 您可以透過擴充套件它來幫助 Wikibooks。 |
與一些基於函式式正規化的程式語言不同,Emacs Lisp 具有大多數開發人員都非常熟悉的可變變數。Emacs Lisp 中的變數是無型別的,這意味著一個變數可以儲存您想賦予它的任何值:數字、字串,甚至函式都可以賦值給變數。同一個變數可以在一個點儲存一個整數,而在幾行程式碼之後儲存一個函式定義(儘管大多數開發人員會認識到這不是好的做法)。
在 Emacs Lisp 中使用變數最簡單的方法是使用全域性變數。顧名思義,這些變數可以在程式的任何地方讀取和寫入,並且永久保留其值。
在 Emacs Lisp 中使用全域性變數時需要注意的一件事是,沒有名稱空間的概念可以用來將您庫中對全域性變數的引用與其他人庫或 Emacs 核心中的全域性變數分開。因此,最好以特定於您庫的字串作為字首來命名您的全域性變數。
全域性變數在 Emacs(以及第三方 Emacs Lisp 包)中被廣泛用於儲存模組的簡單配置設定。雖然無節制地使用全域性變數會使程式碼難以理解,但如果謹慎使用,它提供了一種以最小的開銷來控制配置的方法。用於配置的全域性變數通常會與其相關聯的文件。您可以透過將游標移動到相關變數上並鍵入 C-h v 來訪問此文件。
您可以使用 setq 特殊形式為現有變數賦值或建立新變數
(setq some-variable 12)
setq 形式將其第二個引數的值賦給第一個引數中給出的變數。
- 作用域變數
- let 和 let*
- 緩衝區區域性變數
| 本節內容是一個存根。 您可以透過擴充套件它來幫助 Wikibooks。 |
- 將函式作為引數傳遞
- Lambda 函式
- lambda 和 defun 之間的比較
| 本節內容是一個存根。 您可以透過擴充套件它來幫助 Wikibooks。 |
define-skeleton作為示例
- ↑ 實際上,Emacs 會為您提供完整的回溯資訊,以顯示問題發生的位置;為了清楚起見,這裡省略了詳細資訊,但不要對您的錯誤訊息看起來比這裡描述的更復雜感到驚訝
