前言
這幾天用Scala寫(xiě)了一堆流計(jì)算程序诀豁,在翻閱Scala文檔時(shí)看到了閉包一節(jié)向图,不知怎么就回憶起了自己上大二時(shí)用JavaScript做創(chuàng)新項(xiàng)目的經(jīng)歷——因?yàn)镴S閉包的原理對(duì)當(dāng)時(shí)的我來(lái)說(shuō)很費(fèi)解般又,以至于熬了一整個(gè)通宵才差不多弄明白效拭。正好這幾天博客素材有點(diǎn)缺乏袍啡,那么就總結(jié)一下“閉包”這個(gè)神叨叨的詞背后的東西吧吱韭。
實(shí)際上惧互,閉包的概念同時(shí)存在于離散數(shù)學(xué)和計(jì)算機(jī)科學(xué)的領(lǐng)域中棱烂,并且這兩種“閉包”之間沒(méi)有什么明顯的關(guān)聯(lián)租漂。對(duì)程序員而言,我們接觸到的閉包就是函數(shù)式編程(functional programming)情境下的一種語(yǔ)言特性颊糜,我主要想說(shuō)的也就是這方面哩治。當(dāng)然,離散數(shù)學(xué)那邊的事情也會(huì)抽空聊兩句衬鱼。
要想真正地搞懂閉包业筏,我們就必須追根溯源。所以本文暫時(shí)不會(huì)請(qǐng)出本尊鸟赫,而是介紹兩個(gè)必備的前置知識(shí):一為λ演算驾孔,二為作用域規(guī)則。
λ演算與函數(shù)式編程
λ演算(lambda calculus)是所有函數(shù)式編程語(yǔ)言的基礎(chǔ)惯疙,而閉包又與函數(shù)式編程強(qiáng)相關(guān)翠勉,故有必要最先說(shuō)說(shuō)λ演算。
非正式地來(lái)講霉颠,λ演算是一種“一元函數(shù)生萬(wàn)物”的演算規(guī)則对碌,早在上世紀(jì)30年代由美國(guó)數(shù)學(xué)家阿隆佐·邱奇(Alonzo Church)提出。一言以蔽之蒿偎,λ演算將我們正常理解的計(jì)算過(guò)程抽象為單變量(標(biāo)量)及一元函數(shù)的定義和應(yīng)用過(guò)程朽们。這句話并不容易理解,下面舉個(gè)栗子诉位。
假設(shè)我們有個(gè)最簡(jiǎn)單的函數(shù)f(x) = x + 2骑脱,用λ演算來(lái)表示,如下:
λx.x + 2 (參數(shù)名x可以隨意寫(xiě))
用λ演算表示f(7) = 7 + 2 = 9的過(guò)程苍糠,如下:
(λx.x + 2) 7 = 7 + 2 = 9
如果是像g(x, y) = x - y這樣的二元函數(shù)叁丧,該怎么辦呢?答案是用高階一元函數(shù)。簡(jiǎn)單說(shuō)來(lái)就是拆成兩個(gè)一元函數(shù)來(lái)表示拥娄,其中一個(gè)函數(shù)返回值是函數(shù)蚊锹,如下:
λx.(λy.x - y) (λ演算是左結(jié)合的,所以這里括號(hào)刪掉不會(huì)有歧義)
這其實(shí)就是之前曾簡(jiǎn)單談過(guò)的函數(shù)柯里化(currying)思想稚瘾。用λ演算表示g(7, 2) = 7 - 2 = 5的過(guò)程牡昆,如下:
(λx.λy.x - y) 7 2 = (λy.7 - y) 2 = 7 - 2 = 5
由此可見(jiàn),λ演算實(shí)際上就是反復(fù)的函數(shù)求值過(guò)程摊欠。并且它的基本規(guī)則很簡(jiǎn)單丢烘,只有三種:
- x,定義變量標(biāo)識(shí)符些椒;
- λx.M铅协,通過(guò)表達(dá)式M定義一元函數(shù),其參數(shù)為x摊沉。此時(shí)就說(shuō)變量x在表達(dá)式M中被約束(bound);
- M N痒给,將表達(dá)式N作為參數(shù)應(yīng)用到表達(dá)式M定義的函數(shù)上说墨,也就是平時(shí)說(shuō)的代入求值。
所以苍柏,λ演算的規(guī)則可以用上下文無(wú)關(guān)文法(即喬姆斯基分類(lèi)中的2型文法)表示尼斧,以下是BNF范式的描述,也是相當(dāng)容易:
<表達(dá)式> ::= <標(biāo)識(shí)符>
<表達(dá)式> ::= (λ<標(biāo)識(shí)符>.<表達(dá)式>)
<表達(dá)式> ::= (<表達(dá)式> <表達(dá)式>)
其中標(biāo)識(shí)符從預(yù)先設(shè)定好的標(biāo)識(shí)符集合中取得试吁。括號(hào)用來(lái)表達(dá)運(yùn)算優(yōu)先級(jí)棺棵,在沒(méi)有歧義的情況下可以去掉(比如上文的λx.λy.x - y)。
通過(guò)了解λ演算熄捍,看官要特別注意以下三個(gè)特征:
- 函數(shù)可以賦值給標(biāo)識(shí)符(變量)烛恤;
- 函數(shù)可以作為函數(shù)的參數(shù)進(jìn)行傳遞;
- 函數(shù)的結(jié)果(返回值)可以是函數(shù)余耽。
函數(shù)式編程語(yǔ)言(如JS缚柏、Scala)全部具有這三個(gè)從λ演算繼承而來(lái)的特征,亦即函數(shù)是名正言順的一類(lèi)對(duì)象(first-class object)碟贾,也稱(chēng)作“一等公民”(first-class citizen)币喧,與最常見(jiàn)的int、long等最原始的值類(lèi)型享有相同的地位袱耽。相反地杀餐,我們熟識(shí)的以C系語(yǔ)言為代表的指令式(imperative)編程語(yǔ)言就不具備這些特點(diǎn)。
為什么要煞費(fèi)苦心地搞出λ演算這么一套復(fù)雜的規(guī)則呢朱巨?因?yàn)榍衿媾c和他同時(shí)代的學(xué)者們希望發(fā)明一種通用的計(jì)算模型史翘,通過(guò)它能夠計(jì)算(當(dāng)時(shí)的)任意表達(dá)式的結(jié)果。讀了這句話,看官可能會(huì)想起圖靈和他的圖靈機(jī)恶座。沒(méi)錯(cuò)搀暑,圖靈機(jī)和λ演算(以及前后出現(xiàn)的其他通用計(jì)算模型)都是計(jì)算機(jī)科學(xué)的起源,并且它們的計(jì)算能力是等價(jià)的】缌眨現(xiàn)代編程語(yǔ)言都是圖靈完全的自点,而在λ演算的基礎(chǔ)上,才產(chǎn)生了函數(shù)式編程脉让,意義很重大桂敛。
下圖示出一種機(jī)械式圖靈機(jī)的構(gòu)造。
下表則是邱奇本人發(fā)明的“邱奇數(shù)”(Church numeral)的λ演算定義及基礎(chǔ)運(yùn)算規(guī)則溅潜。
由此可見(jiàn)术唬,自然數(shù)n其實(shí)就是將函數(shù)f應(yīng)用n次的高階函數(shù),說(shuō)明λ演算確實(shí)是一種有效的計(jì)算模型滚澜。為了避免陷入太多細(xì)節(jié)跳不出來(lái)粗仓,如果想看更多的內(nèi)容(比如邱奇數(shù)是否可以表示負(fù)數(shù)),還請(qǐng)參見(jiàn)上面的傳送門(mén)设捐。
編程語(yǔ)言的作用域規(guī)則
作用域
通俗來(lái)講借浊,作用域(scope)就是指程序源碼中標(biāo)識(shí)符的定義有效的那部分區(qū)域。平時(shí)我們?cè)谡f(shuō)“全局變量”萝招、“本地變量”這些詞時(shí)蚂斤,其實(shí)都是在描述它們的作用域,即在全局有效槐沼、在本地有效曙蒸。當(dāng)然,能夠用標(biāo)識(shí)符代表的東西不止變量岗钩,還有類(lèi)纽窟、命名空間、函數(shù)兼吓、方法等等等等师倔。但是在本文中,為了不致使問(wèn)題復(fù)雜化周蹭,我們只關(guān)心變量和函數(shù)的作用域趋艘。
下面用JavaScript舉個(gè)例子(因?yàn)樗Z(yǔ)法比Scala簡(jiǎn)單得多),圖中每個(gè)不同顏色的框就代表一個(gè)作用域凶朗。
理解了作用域瓷胧,那么一門(mén)編程語(yǔ)言的作用域規(guī)則自然就是確定標(biāo)識(shí)符作用域的法則。目前只有兩種作用域規(guī)則棚愤,分別來(lái)看看搓萧。
詞法作用域規(guī)則
考慮以下JS代碼杂数。
var p = 1;
function f() {
console.log("p in f(): " + p);
p = 2;
}
function g() {
var p = 3;
f();
console.log("p in g(): " + p);
}
g();
console.log("p: " + p);
執(zhí)行結(jié)果如下。
> "p in f(): 1"
> "p in g(): 3"
> "p: 2"
該結(jié)果顯然是符合“常理”的瘸洛。這是因?yàn)榘↗S在內(nèi)的絕大多數(shù)語(yǔ)言都遵循詞法作用域規(guī)則(lexical scoping)揍移。
以上面代碼為例,函數(shù)g()執(zhí)行時(shí)反肋,會(huì)調(diào)用函數(shù)f()那伐。雖然f()在g()的作用域內(nèi)被調(diào)用,但是它的執(zhí)行不會(huì)受到g()作用域環(huán)境的影響石蔗,所以f()向上在全局作用域內(nèi)找到初值為1的變量p罕邀,并執(zhí)行賦值為2的操作。而g()在執(zhí)行時(shí)养距,首先找到的是自己作用域內(nèi)定義的那個(gè)p诉探,所以最終輸出是3。
也就是說(shuō)棍厌,在詞法作用域規(guī)則下肾胯,作用域是嚴(yán)格按照標(biāo)識(shí)符(函數(shù))的定義來(lái)的——因?yàn)閒()和g()一樣被定義在全局作用域內(nèi),所以f()并不是g()的一部分耘纱。只有當(dāng)f()嵌套定義在g()內(nèi)部時(shí)敬肚,才能視為g()的一部分。標(biāo)識(shí)符的定義就是代碼揣炕,從代碼能直觀地看出作用域的范圍,這就是lexical一詞的由來(lái)东跪。另外畸陡,作用域在代碼編譯期就可以確定不變,故詞法作用域規(guī)則又叫靜態(tài)作用域規(guī)則(static scoping)虽填。
動(dòng)態(tài)作用域規(guī)則
將上一小節(jié)的JS代碼改寫(xiě)為Shell丁恭,如下。
p=1
function f() {
echo "p in f(): $p";
p=2;
}
function g() {
local p=3;
f;
echo "p in g(): $p";
}
g
echo "p: $p"
執(zhí)行結(jié)果如下斋日。
p in f(): 3
p in g(): 2
p: 1
神奇了牲览,為什么會(huì)是3、2恶守、1第献?
這是因?yàn)橐許hell為代表的一小撮語(yǔ)言采用的是動(dòng)態(tài)作用域規(guī)則(dynamic scoping)。與詞法作用域規(guī)則相反兔港,動(dòng)態(tài)作用域規(guī)則按照標(biāo)識(shí)符(函數(shù))的調(diào)用來(lái)區(qū)分作用域庸毫。函數(shù)f()在g()內(nèi)部被調(diào)用時(shí),它就受到了g()作用域的影響衫樊,看到的變量p不再是全局作用域內(nèi)的變量p飒赃,而是g()內(nèi)定義的那個(gè)利花,全局的p最終沒(méi)有被操作。也就是說(shuō)载佳,動(dòng)態(tài)作用域是在程序運(yùn)行期確定的炒事,代碼本身可能無(wú)法反映出正確的作用域。
動(dòng)態(tài)作用域規(guī)則并不是重點(diǎn)蔫慧。在后文探索閉包的過(guò)程中挠乳,我們就會(huì)理解它與λ演算和詞法作用域規(guī)則之間的關(guān)系了。
To be continued
為了讓自己能夠善始善終藕漱,以及留個(gè)小懸念欲侮,最后引入一個(gè)經(jīng)典的關(guān)于閉包與對(duì)象的故事。Scheme語(yǔ)言大佬Anton van Straaten在這里寫(xiě)道:
The venerable master Qc Na was walking with his student, Anton. Hoping to
prompt the master into a discussion, Anton said "Master, I have heard that
objects are a very good thing - is this true?" Qc Na looked pityingly at
his student and replied, "Foolish pupil - objects are merely a poor man's
closures."
Chastised, Anton took his leave from his master and returned to his cell,
intent on studying closures. He carefully read the entire "Lambda: The
Ultimate..." series of papers and its cousins, and implemented a small
Scheme interpreter with a closure-based object system. He learned much, and
looked forward to informing his master of his progress.
On his next walk with Qc Na, Anton attempted to impress his master by
saying "Master, I have diligently studied the matter, and now understand
that objects are truly a poor man's closures." Qc Na responded by hitting
Anton with his stick, saying "When will you learn? Closures are a poor man's
object." At that moment, Anton became enlightened.
“對(duì)象是窮人的閉包”肋联、“閉包是窮人的對(duì)象”威蕉,這兩句看似對(duì)立的話實(shí)際上都是正確的。只有合二為一橄仍,才能說(shuō)真正理解了它們之間的異同韧涨。Anton本人也認(rèn)為,對(duì)象與閉包的問(wèn)題是非常有禪意(koan)在其中的侮繁。欲知后事如何虑粥,且聽(tīng)下回分解吧。
晚安宪哩。