Emacs之魂(四):標識符,符號和變數

1. 符號

上文我們提到了Emacs Lisp是一種Lisp-2,

即同一個符號(symbol)在不同的上下文中,可以分別表示兩種不同的值(value):

變數(variable)或者函數(function),

這裡符號(symbol)實際上是一個Lisp對象,而它的文本表示(textual representation)稱之為標識符(identifier)。

標識符,符號和變數,這三個概念如果不謹慎對待,就會造成混亂。

其它編程語言可能沒有「符號」的概念,這也是學習Lisp時容易困惑的原因之一。

此外,這裡「符號」特指Lisp語言的「Symbol」,不能用漢語字面意思來理解它。

標識符,是Lisp的上下文無關文法(context-free grammar)中的一個非終結符(nonterminal),

它是一種詞法結構,編譯器前端(compiler front-end)在進行詞法分析時會將標識符從字元流中識別出來。

符號(symbol)是一個Lisp對象,它是一個數據結構,由以下4個部分組成,

(1)name:symbol的名字

(2)value cell:作為一個動態變數,symbol的值

(3)function cell:作為一個函數,它的函數值

(4)property list:屬性列表

標識符直接在Lisp代碼中出現,會被讀取為一個符號(symbol), 然後在不同的上下文中,Lisp求值器會看情況取出value cell或者function cell的內容, 作為該符號(symbol)的值(value)。

如果某一個函數接受符號(symbol)而不是它的值(value)作為參數,我們就得引用(quote)它,

即,我們使用引用,可以創建一個符號(symbol)字面量(literal)。

例如:symbol-name函數可以用來獲取符號(symbol)x的name,

(symbol-name x)"x"

結合上一篇,我們總結如下,

(1)直接寫(foo bar bar)表示函數調用或者宏調用

(2)加引用(foo bar bar)表示列表

(3)直接寫x表示變數或者函數

(4)加引用x表示符號(symbol)

如果只是這樣的話,還很容易理解的,

可是value cell中只能保存動態變數,這一點理解起來就比較困難了。

「動態」是什麼意思呢?還要從變數的定義和類別說起。

2. 全局變數和局部變數

Lisp提供了兩種定義變數的方式,defvarlet

其中defvar用來定義全局變數,let用來定義局部變數。

例子:

(defvar a "1")(let ((b "2")) (message "%s" b)) ; "2"(message "%s" a) ; "1"(message "%s" b) ; Error: Symbol』s value as variable is void: b

以上程序中,我們用defvar定義了全局變數a,和局部變數b

其中message用於在Emacs的「echo area」中輸出內容,

message的第一個參數是表示格式的字元串,第二個參數是待輸出的內容。

Lisp用分號表示注釋。

為了執行這段程序,我們需要將它寫到Emacs的buffer中,然後按M-x再輸入eval-buffer回車,來求值整個緩衝區。

其中M-x表示按住alt鍵,然後再按x,該快捷鍵命令會將游標定位到echo area,等待用戶輸入一個函數名,

我們輸入函數eval-buffer,它用來求值當前buffer,

它還有一個別名為ev-b,可以記為M-x ev-b

注意,按M-x之後,我們不用輸入「M-x」,直接輸入函數名「ev-b」就可以了。

程序最終的執行結果如注釋所示,變數a在整個程序中可用,而變數b只在let範圍內可用。

3. 作用域和生存期

以上程序中,我們通過defvarlet,讓a的值為字元串"1"b的值為字元串"2"

我們說,defvarlet建立了兩個綁定(binding),將a綁定為"1"b綁定為"2"

The association between a variable and its value is called a binding.

——《Essentials of Programming Languages - P90》

變數除了可以分為全局變數和局部變數之外,還有另外兩方面的屬性,作用域(scope)和生存期(extent)。

作用域表示,在源代碼文本中,綁定在什麼地方(where)有效。 生存期表示,在程序執行的過程中,綁定在什麼時候(when)有效。

Emacs Lisp支持兩種形式的綁定,

動態綁定(dynamic binding)和靜態綁定(lexical binding)。

動態綁定具有動態作用域和動態生存期,

動態作用域(dynamic scope),任何一段代碼都可能訪問變數的綁定,

動態生存期(dynamic extent),只有在綁定結構(例如let)執行的過程中,綁定才有效。

靜態綁定具有靜態作用域(也稱詞法作用域)和無限生存期,

詞法作用域(lexical scope),綁定在綁定結構的源代碼文本範圍中有效,

無限生存期(indefinite extent),某些情況下,綁定可能永遠有效。

幸運的是,Emacs Lisp同時支持這兩種綁定方式,否則很難直觀的理解它們,

默認情況下Emacs Lisp支持動態綁定,我們還可以為Emacs啟用靜態綁定規則。

3.1 動態綁定

例子:

(defvar x 0)(defun getx () x)(let ((x 1)) (getx)) ; 1(getx) ; 0

其中defun用於在Emacs Lisp中定義函數,以上代碼定義了一個getx無參數函數,

(getx)是對該函數的調用。

在對getx進行的第一次調用時,函數中引用了自由變數x,Lisp要尋找程序執行期間對x最近的綁定,

於是找到了let表達式中,getx調用之前對x的綁定,為1

第二次調用getx時,let表達式的執行已經結束了,它對任何變數的綁定都將銷毀,

這時候再調用getx,程序執行期間最近的對x的綁定,是(defvar x 0)x的綁定,為0

在Emacs Lisp中,每一個符號(symbol)都有一個value cell,表示變數的當前值(current dynamic value),當一個符號(symbol)被給定一個局部綁定時(dynamic local binding),Emacs會把原來的value cell記錄在一個棧上,然後把新值放入value cell中。當綁定結構(例如let)執行完後,Emacs進行彈棧操作,取出舊的值放回value cell中。

注意,其他語言中的全局變數並不是動態綁定,考慮以下JavaScript代碼,

let x = 0;function getx(){ return x;}((x)=>{ getx(); // 0})(1);getx(); // 0

JavaScript的全局變數仍然是靜態綁定,第一個getx被調用時,並不會攜帶x的任何信息過去。

getx總是從源代碼文本範圍內尋找x,JavaScript對變數採用的是靜態綁定。

3.2 靜態綁定

例子:

; -*- lexical-binding: t -*-(setq test (let ((foo "bar")) (lambda () foo)))(let ((foo "something-else")) (funcall test)) ; "bar"(funcall test) ; "bar"

其中,; -*- lexical-binding: t -*-是Emacs的文件變數(file variable),

用於對當前文件或buffer啟用靜態綁定規則,它必須位於文件或者buffer的第一行

在調用test函數時,函數中引用的自由變數foo,總是從源代碼文本範圍內離該函數最近的位置尋找,

於是找到了(lambda () foo)外層let中綁定的"bar"

所以兩次對test的調用,結果都是"bar"

在Emacs Lisp中,每一個綁定結構都會創建一個新的詞法環境(lexical environment),在這個環境中,保存了變數名和它所對應值之間的對應關係(即,綁定關係),當Lisp求值器對某個符號(symbol)求值的時候,它首先從詞法環境中尋找值,如果找到了,就用這個值。否則就認為這個符號(symbol)是一個動態變數,讀取符號(symbol)的value cell作為變數的值。

4. 全局變數的動態性質

(1)動態綁定變數的值總是從符號(symbol)的value cell中獲取,而靜態綁定變數的值從詞法環境中獲取。

所以,無法使用symbol-value獲取靜態綁定變數的值。

; -*- lexical-binding: t -*-(let ((x 1)) (symbol-value x)) ; Symbol』s value as variable is void: x

(2)即使啟用了變數的靜態綁定規則,全局變數仍然是動態綁定的。 let並沒有引入新的靜態變數x,而是,建立了局部動態變數x,然後用局部動態變數遮擋了全局動態變數的值。

; -*- lexical-binding: t -*-(setq test (let ((x 1)) (lambda () x)))(funcall test) ; 1; -*- lexical-binding: t -*-(defvar x 0)(setq test (let ((x 1)) (lambda () x)))(funcall test) ; 0

以上兩段程序都啟用了靜態綁定規則,第一段程序中的x是靜態綁定的,

第二段程序中的x是全局變數,使用defvar定義了,所以它是動態綁定的。

在進行試驗時,需要在全新的buffer中,分別測試,

否則(defvar x 0)一旦執行,即使再重新M-x eval-bufferx的值已經被定義了。


參考

GNU Emacs manual

GNU Emacs Lisp Reference Manual

Essentials of Programming Languages

推薦閱讀:

TAG:符號 | elisp | 綁定 |