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語言中一種常用的數據結構,用來表示一批數據。
例如,由整數1
,2
和3
構成的列表對象,Lisp會將它列印為,(1 2 3)
。各個列表元素用空格分隔,用圓括弧括起來。然而,在Lisp代碼中直接寫(1 2 3)
,並不會創建一個列表對象,
(+ 1 2)
表示對整數1
和2
進行加法運算。那麼如何才能創建一個列表對象呢?
我們需要調用list
函數,(list 1 2 3)
,這段代碼將會創建一個由整數1
,2
和3
構成的列表對象,
這個列表對象列印為(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」表示真值,
其它語言中,也提供了廣泛的字面量表示法,例如,JavaScript提供了數組和對象字面量,
const obj = { x: 1, y: [2, 3, 4]};
這段代碼創建了一個JavaScript Object
,它的x
屬性值是1
,y
屬性值是一個數組。
new Object
和new Array
來創建它。Lisp中列表對象用的非常多,每次都使用list
函數來創建是一件麻煩的事情,
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)
都首先要被求值,分別求值為3
和7
,然後再進行乘法運算,結果為21
。而宏和內置的特殊命令,並不要求如此,它們有自己的對參數的求值策略。
其中「1」稱之為自求值對象,對它進行求值將得到它本身,
1(eval 1)(eval (eval 1))
其結果都為1
,其它的自求值對象還包括布爾值,字元串,向量,等等。
(+ 1 2)
中1
和2
之前沒有quote
,是因為它們是自求值對象,(+ 1 2)
和(+ 1 2)
的計算結果是相等的。
7. 在Emacs中求值表達式
Emacs可以直接求值文本中的Lisp代碼,我們只需要將游標定位到列表尾部,
然後按快捷鍵C-x C-e
即可。(指的是按Ctrl+x
,然後再按Ctrl+e
我們還可以試試quote
和自求值對象,1
求值為1
,1
求值為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推薦閱讀: