[Emacs] Emacs之魂(四):標(biāo)識符笛厦,符號和變量

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提供了兩種定義變量的方式,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”中輸出內(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. 作用域和生存期

以上程序中牍帚,我們通過defvarlet,讓a的值為字符串"1"乳蛾,b的值為字符串"2"履羞,
我們說,defvarlet建立了兩個(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末馆揉,一起剝皮案震驚了整個(gè)濱河市业舍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌升酣,老刑警劉巖舷暮,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異拗踢,居然都是意外死亡脚牍,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門巢墅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诸狭,“玉大人,你說我怎么就攤上這事君纫⊙庇觯” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵蓄髓,是天一觀的道長叉庐。 經(jīng)常有香客問我,道長会喝,這世上最難降的妖魔是什么陡叠? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮肢执,結(jié)果婚禮上枉阵,老公的妹妹穿的比我還像新娘。我一直安慰自己预茄,他們只是感情好兴溜,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著耻陕,像睡著了一般拙徽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上诗宣,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天膘怕,我揣著相機(jī)與錄音,去河邊找鬼召庞。 笑死淳蔼,一個(gè)胖子當(dāng)著我的面吹牛侧蘸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鹉梨,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼讳癌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了存皂?” 一聲冷哼從身側(cè)響起晌坤,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎旦袋,沒想到半個(gè)月后骤菠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疤孕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年商乎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祭阀。...
    茶點(diǎn)故事閱讀 40,664評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鹉戚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出专控,到底是詐尸還是另有隱情抹凳,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布伦腐,位于F島的核電站赢底,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏柏蘑。R本人自食惡果不足惜幸冻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咳焚。 院中可真熱鬧嘁扼,春花似錦、人聲如沸黔攒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽督惰。三九已至,卻和暖如春旅掂,著一層夾襖步出監(jiān)牢的瞬間赏胚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工商虐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留觉阅,地道東北人崖疤。 一個(gè)月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像典勇,于是被迫代替她去往敵國和親劫哼。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容