Emacs之魂(三):列表,引用和求值策略

回顧

上文我們介紹了Emacs的用法,發現一分鐘學會使用它並不是難事,

而且,我們沒有讓快捷鍵束縛住,因為Emacs的精髓在於Emacs Lisp中。

本文我們開始探討Emacs Lisp,不過在這之前我們還要先熟悉一下Lisp的特點和Lisp家族的成員,

隨後本文重點分析和介紹了列表,引用和求值策略,

這幾個概念,尤其是引用,對學習者來說非常容易引起困惑,

本文採用了不同的角度來描述這些概念。

1. 強大的Lisp

1960年,John McCarthy發表了一篇計算機領域的文章,這是一篇「驚世之作」,

它的作用簡直就像歐幾里得《幾何原本》對幾何學的貢獻一樣。

John McCarthy只用了一些簡單的的運算符和函數,構建出了一門圖靈完備的編程語言,

稱之為Lisp,Lisp是列表處理(List Processing)的簡稱。

這門語言的關鍵思想是,不論代碼還是數據,都用統一的數據結構(列表)進行表示。

Lisp語言具有很強的表達能力,我們可以用更少的代碼做更多的事情。

通常而言,語言具有表達能力就必須提供豐富的內置功能和強大的擴展性。

語言的內置功能指的是語言默認提供的功能,它能減少程序員的重複勞動,幫助他們快速完成工作。

語言的擴展性,指的是當語言內置功能不能滿足需要的時候,程序員可以怎樣做。

同時具有豐富的功能和強大的擴展性是很困難的,這需要在語言的設計階段就考慮好,

語言的內置功能越多,就會越複雜,擴展功能的與內置功能的一致性就很難被保證。

現代的高級編程語言,離不開編譯器和解釋器,

編譯器將高級語言的代碼轉換成更底層的語言,例如C語言或者彙編,

解釋器提供了一個運行時環境,直接解釋執行高級語言的源代碼。

一般而言,編譯器是由語言的開發商提供的,使用者並不會參與到編譯器的開發工作之中,

如果想要在語言中支持一等函數(first-class function),就必須讓語言的開發商改寫編譯器,

如果需要增加新的類似if-else的控制結構,或者讓語言支持面向對象編程,也要改寫編譯器才行。

因此,語言支持什麼功能,以及源代碼被如何編譯,完全取決於開發商。

而Lisp語言則不同,它允許程序員對編譯器進行編程,(元編程

Lisp程序員可以決定代碼被如何編譯甚至如何被讀取,像是半個編譯器的開發者一樣。

2. Lisp-1和Lisp-2

Lisp語言構成了一個家族,具有成百上千種方言,

用的最多的幾種是,Common Lisp,Scheme,Racket,和Emacs Lisp。

其中Scheme的目標是簡潔,Common Lisp提供了強大的工業級支持,

Racket提供了一種創造語言和設計實踐的平台,Emacs Lisp主要用於Emacs中。

Emacs Lisp更像Common Lisp,它們都是Lisp-2,

即同一個符號在不同的上下文中,可以分別用來表示變數和函數,

而Scheme和Racket則只能用來表示同一個實體,稱為Lisp-1。

出現Lisp-2,主要是因為有效率方面的考慮,

在Lisp-2中,函數和變數分屬不同的名字空間,在不同的環境中,由不同的求值器進行處理。

這樣做也使語義更加複雜了,以後的文章中,我們會介紹Emacs Lisp中符號(Symbol)的概念。

3. 列表對象和它的文本表示

列表是Lisp語言中一種常用的數據結構,用來表示一批數據。

例如,由整數123構成的列表對象,Lisp會將它列印為,(1 2 3)

各個列表元素用空格分隔,用圓括弧括起來。

然而,在Lisp代碼中直接寫(1 2 3),並不會創建一個列表對象,

因為Lisp程序也是用括弧方式表示的,例如,(+ 1 2)表示對整數12進行加法運算。

那麼如何才能創建一個列表對象呢?

我們需要調用list函數,(list 1 2 3),這段代碼將會創建一個由整數123構成的列表對象,

這個列表對象列印為(1 2 3)

注意,以上我們嚴謹的區分了Lisp對象和它的列印結果,

是因為對象和它的文本表示(textual representation)是不同的概念。

例如,在C語言中我們寫,

int result = 1 + 2;

我們實際上是用「1」表示了整數1,「1」只是一段文本,是印刷符號,而整數1是一個數學對象,

同樣的,「+」是一段文本,它表示了加法運算符。

在Lisp語言中,我們用文本來寫程序,而Lisp讀取器得到的是Lisp對象,

經過對這些Lisp對象進行計算,得到了計算結果,也是一個Lisp對象,

最終,反饋給我們的是這個對象的文本表示。

4. 字面量和引用

在Lisp中,我們用文本「1」可以直接表示整數1,用「#t」表示真值,

類似的「1」和「#t」,稱之為對象的字面量表示(literal representation)。

其它語言中,也提供了廣泛的字面量表示法,例如,JavaScript提供了數組和對象字面量,

const obj = { x: 1, y: [2, 3, 4]};

這段代碼創建了一個JavaScript Object,它的x屬性值是1y屬性值是一個數組。

字面量表示法,使得我們不必調用new Objectnew Array來創建它。

Lisp中列表對象用的非常多,每次都使用list函數來創建是一件麻煩的事情,

因此,Lisp語言提供了列表對象的字面量寫法,我們只需要調用quote就可以了。

(quote (1 2 3))

以上Lisp代碼會創建一個列印形式為(1 2 3)的列表對象。

對於嵌套列表,使用quote是非常方便的,

(quote (1 (2 3) ((4 5) (6 7))))

像這樣創建列表的方式,稱為引用(quoting),

這不同於按引用調用(call by reference)中的「引用」(reference)。

quote還有一個便捷的寫法,就是用單引號來表示它,(quote (1 2 3))可以表示為,

(1 2 3)

我們只需要在列表前加一個單引號即可,因為列表的右括弧表明了它在引用這個列表。

5. quote和list

值得一提的是,引用並不保證每次都會重新創建列表。

例如,在Emacs Lisp中我們使用defun創建函數,

(defun foo () (1 2 3))

然後,用以下方式進行函數調用,注意foo參數個數為0個,

(foo)

多次調用foo,編譯器可能返回同一個列表對象。

list則不同,每次調用它會返回一個全新的列表對象,

(defun foo () (list 1 2 3))

6. 求值策略

Lisp代碼是由表達式構成的,Lisp程序的執行過程就是表達式的求值過程,

(* (+ 1 2) (+ 3 4))

以上表達式的求值結果為21

在程序的列表表示法中,從左到右位於第一個位置的元素,是比較特殊的,

它表示一個函數(function),一個宏(macro),或者一個內置的特殊命令(special form)。

位於其他位置的元素稱為參數。

函數被調用的時候,它的每個參數都必須首先被求值,

例如,以上程序中*+都是函數,

在調用乘法函數*時,它的參數(+ 1 2)(+ 3 4)都首先要被求值,分別求值為37

然後再進行乘法運算,結果為21

而宏和內置的特殊命令,並不要求如此,它們有自己的對參數的求值策略。

其中「1」稱之為自求值對象,對它進行求值將得到它本身,

1(eval 1)(eval (eval 1))

其結果都為1,其它的自求值對象還包括布爾值,字元串,向量,等等。

(+ 1 2)12之前沒有quote,是因為它們是自求值對象,(+ 1 2)(+ 1 2)的計算結果是相等的。

7. 在Emacs中求值表達式

Emacs可以直接求值文本中的Lisp代碼,我們只需要將游標定位到列表尾部,

然後按快捷鍵C-x C-e即可。(指的是按Ctrl+x,然後再按Ctrl+e

我們還可以試試quote和自求值對象,1求值為11求值為1

然而1卻求值為(quote 1),是因為1實際是(quote (quote 1))

它表示用字面量方式創建了一個形如(quote 1)的列表對象。

下文,我們來討論Emacs Lisp的控制結構和基本的數據類型,

使用Lisp編程是一件有趣的事情。


參考

The Roots of Lisp

Land of Lisp

Lisp in small pieces

An Introduction to Programming in Emacs Lisp

GNU Emacs Lisp Reference Manual


推薦閱讀:

同樣的函數引用里為什麼使用string引用時會清空數據?
【VBA初學者教程】- 第一章 VBA入門知識:引用工作表

TAG:Lisp | 求值策略 | 引用 |