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提供了兩種定義變數的方式,defvar
和let
,
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. 作用域和生存期
以上程序中,我們通過defvar
和let
,讓a
的值為字元串"1"
,b
的值為字元串"2"
,
defvar
和let
建立了兩個綁定(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),
在調用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-buffer
,x
的值已經被定義了。
參考
GNU Emacs manual
GNU Emacs Lisp Reference Manual Essentials of Programming Languages推薦閱讀: