1. 符號
上文我們提到了Emacs Lisp是一種Lisp-2乌庶,
即同一個(gè)符號(symbol)在不同的上下文中果元,可以分別表示兩種不同的值(value):
變量(variable)或者函數(shù)(function),
這里符號(symbol)實(shí)際上是一個(gè)Lisp對象腾降,而它的文本表示(textual representation)稱之為標(biāo)識符(identifier)拣度。
標(biāo)識符,符號和變量螃壤,這三個(gè)概念如果不謹(jǐn)慎對待抗果,就會造成混亂。
其它編程語言可能沒有“符號”的概念奸晴,這也是學(xué)習(xí)Lisp時(shí)容易困惑的原因之一冤馏。
此外,這里“符號”特指Lisp語言的“Symbol”寄啼,不能用漢語字面意思來理解它逮光。
標(biāo)識符,是Lisp的上下文無關(guān)文法(context-free grammar)中的一個(gè)非終結(jié)符(nonterminal)墩划,
它是一種詞法結(jié)構(gòu)涕刚,編譯器前端(compiler front-end)在進(jìn)行詞法分析時(shí)會將標(biāo)識符從字符流中識別出來。
符號(symbol)是一個(gè)Lisp對象走诞,它是一個(gè)數(shù)據(jù)結(jié)構(gòu)副女,由以下4個(gè)部分組成,
(1)name:symbol的名字
(2)value cell:作為一個(gè)動態(tài)變量,symbol的值
(3)function cell:作為一個(gè)函數(shù)碑幅,它的函數(shù)值
(4)property list:屬性列表
標(biāo)識符直接在Lisp代碼中出現(xiàn)戴陡,會被讀取為一個(gè)符號(symbol),
然后在不同的上下文中沟涨,Lisp求值器會看情況取出value cell或者function cell的內(nèi)容恤批,
作為該符號(symbol)的值(value)。
如果某一個(gè)函數(shù)接受符號(symbol)而不是它的值(value)作為參數(shù)裹赴,我們就得引用(quote)它喜庞,
即缅茉,我們使用引用总滩,可以創(chuàng)建一個(gè)符號(symbol)字面量(literal)。
例如:symbol-name
函數(shù)可以用來獲取符號(symbol)x
的name棠耕,
(symbol-name 'x)
"x"
結(jié)合上一篇睛竣,我們總結(jié)如下晰房,
(1)直接寫(foo bar bar)
表示函數(shù)調(diào)用或者宏調(diào)用
(2)加引用'(foo bar bar)
表示列表
(3)直接寫x
表示變量或者函數(shù)
(4)加引用'x
表示符號(symbol)
如果只是這樣的話,還很容易理解的射沟,
可是value cell中只能保存動態(tài)變量殊者,這一點(diǎn)理解起來就比較困難了。
“動態(tài)”是什么意思呢验夯?還要從變量的定義和類別說起猖吴。
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”中輸出內(nèi)容域仇,
message
的第一個(gè)參數(shù)是表示格式的字符串刑然,第二個(gè)參數(shù)是待輸出的內(nèi)容。
Lisp用分號表示注釋暇务。
為了執(zhí)行這段程序泼掠,我們需要將它寫到Emacs的buffer中,然后按M-x
再輸入eval-buffer
回車垦细,來求值整個(gè)緩沖區(qū)择镇。
其中M-x
表示按住alt鍵,然后再按x括改,該快捷鍵命令會將光標(biāo)定位到echo area腻豌,等待用戶輸入一個(gè)函數(shù)名,
我們輸入函數(shù)eval-buffer
,它用來求值當(dāng)前buffer吝梅,
它還有一個(gè)別名為ev-b
虱疏,可以記為M-x ev-b
。
注意苏携,按M-x
之后做瞪,我們不用輸入“M-x”,直接輸入函數(shù)名“ev-b”就可以了右冻。
程序最終的執(zhí)行結(jié)果如注釋所示装蓬,變量a
在整個(gè)程序中可用,而變量b
只在let
范圍內(nèi)可用纱扭。
3. 作用域和生存期
以上程序中牍帚,我們通過defvar
和let
,讓a
的值為字符串"1"
乳蛾,b
的值為字符串"2"
履羞,
我們說,defvar
和let
建立了兩個(gè)綁定(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)有效唇聘。
生存期表示版姑,在程序執(zhí)行的過程中,綁定在什么時(shí)候(when)有效迟郎。
Emacs Lisp支持兩種形式的綁定剥险,
動態(tài)綁定(dynamic binding)和靜態(tài)綁定(lexical binding)。
動態(tài)綁定具有動態(tài)作用域和動態(tài)生存期宪肖,
動態(tài)作用域(dynamic scope)表制,任何一段代碼都可能訪問變量的綁定,
動態(tài)生存期(dynamic extent)控乾,只有在綁定結(jié)構(gòu)(例如let)執(zhí)行的過程中么介,綁定才有效。
靜態(tài)綁定具有靜態(tài)作用域(也稱詞法作用域)和無限生存期蜕衡,
詞法作用域(lexical scope)壤短,綁定在綁定結(jié)構(gòu)的源代碼文本范圍中有效,
無限生存期(indefinite extent),某些情況下久脯,綁定可能永遠(yuǎn)有效纳胧。
幸運(yùn)的是,Emacs Lisp同時(shí)支持這兩種綁定方式桶现,否則很難直觀的理解它們躲雅,
默認(rèn)情況下Emacs Lisp支持動態(tài)綁定,我們還可以為Emacs啟用靜態(tài)綁定規(guī)則骡和。
3.1 動態(tài)綁定
例子:
(defvar x 0)
(defun getx ()
x)
(let ((x 1))
(getx)) ; 1
(getx) ; 0
其中defun
用于在Emacs Lisp中定義函數(shù)相赁,以上代碼定義了一個(gè)getx
無參數(shù)函數(shù),
(getx)
是對該函數(shù)的調(diào)用慰于。
在對getx
進(jìn)行的第一次調(diào)用時(shí)钮科,函數(shù)中引用了自由變量x
,Lisp要尋找程序執(zhí)行期間對x
最近的綁定婆赠,
于是找到了let
表達(dá)式中绵脯,getx
調(diào)用之前對x
的綁定,為1
休里。
第二次調(diào)用getx
時(shí)蛆挫,let
表達(dá)式的執(zhí)行已經(jīng)結(jié)束了,它對任何變量的綁定都將銷毀妙黍,
這時(shí)候再調(diào)用getx
悴侵,程序執(zhí)行期間最近的對x
的綁定,是(defvar x 0)
對x
的綁定拭嫁,為0
可免。
在Emacs Lisp中,每一個(gè)符號(symbol)都有一個(gè)value cell做粤,表示變量的當(dāng)前值(current dynamic value)浇借,當(dāng)一個(gè)符號(symbol)被給定一個(gè)局部綁定時(shí)(dynamic local binding),Emacs會把原來的value cell記錄在一個(gè)棧上怕品,然后把新值放入value cell中妇垢。當(dāng)綁定結(jié)構(gòu)(例如
let
)執(zhí)行完后,Emacs進(jìn)行彈棧操作堵泽,取出舊的值放回value cell中修己。
注意,其他語言中的全局變量并不是動態(tài)綁定迎罗,考慮以下JavaScript代碼睬愤,
let x = 0;
function getx(){
return x;
}
((x)=>{
getx(); // 0
})(1);
getx(); // 0
JavaScript的全局變量仍然是靜態(tài)綁定,第一個(gè)getx
被調(diào)用時(shí)纹安,并不會攜帶x
的任何信息過去尤辱。
getx
總是從源代碼文本范圍內(nèi)尋找x
砂豌,JavaScript對變量采用的是靜態(tài)綁定。
3.2 靜態(tài)綁定
例子:
; -*- 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)阳距,
用于對當(dāng)前文件或buffer啟用靜態(tài)綁定規(guī)則,它必須位于文件或者buffer的第一行结借。
在調(diào)用test
函數(shù)時(shí)筐摘,函數(shù)中引用的自由變量foo
,總是從源代碼文本范圍內(nèi)離該函數(shù)最近的位置尋找船老,
于是找到了(lambda () foo)
外層let
中綁定的"bar"
咖熟,
所以兩次對test
的調(diào)用,結(jié)果都是"bar"
柳畔。
在Emacs Lisp中馍管,每一個(gè)綁定結(jié)構(gòu)都會創(chuàng)建一個(gè)新的詞法環(huán)境(lexical environment),在這個(gè)環(huán)境中薪韩,保存了變量名和它所對應(yīng)值之間的對應(yīng)關(guān)系(即确沸,綁定關(guān)系),當(dāng)Lisp求值器對某個(gè)符號(symbol)求值的時(shí)候俘陷,它首先從詞法環(huán)境中尋找值罗捎,如果找到了,就用這個(gè)值拉盾。否則就認(rèn)為這個(gè)符號(symbol)是一個(gè)動態(tài)變量宛逗,讀取符號(symbol)的value cell作為變量的值。
4. 全局變量的動態(tài)性質(zhì)
(1)動態(tài)綁定變量的值總是從符號(symbol)的value cell中獲取盾剩,而靜態(tài)綁定變量的值從詞法環(huán)境中獲取。
所以替蔬,無法使用symbol-value
獲取靜態(tài)綁定變量的值告私。
; -*- lexical-binding: t -*-
(let ((x 1))
(symbol-value 'x)) ; Symbol’s value as variable is void: x
(2)即使啟用了變量的靜態(tài)綁定規(guī)則,全局變量仍然是動態(tài)綁定的承桥。
let
并沒有引入新的靜態(tài)變量x
驻粟,而是,建立了局部動態(tài)變量x
凶异,然后用局部動態(tài)變量遮擋了全局動態(tài)變量的值蜀撑。
; -*- 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
以上兩段程序都啟用了靜態(tài)綁定規(guī)則,第一段程序中的x
是靜態(tài)綁定的剩彬,
第二段程序中的x
是全局變量酷麦,使用defvar
定義了,所以它是動態(tài)綁定的喉恋。
在進(jìn)行試驗(yàn)時(shí)沃饶,需要在全新的buffer中母廷,分別測試,
否則(defvar x 0)
一旦執(zhí)行糊肤,即使再重新M-x eval-buffer
琴昆,x
的值已經(jīng)被定義了。
參考
GNU Emacs manual
GNU Emacs Lisp Reference Manual
Essentials of Programming Languages