理解閉包的前置條件—— λ演算和作用域規(guī)則

前言

這幾天用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)下回分解吧。

晚安宪哩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末娩贷,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子锁孟,更是在濱河造成了極大的恐慌彬祖,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件品抽,死亡現(xiàn)場(chǎng)離奇詭異储笑,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)圆恤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)突倍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人盆昙,你說(shuō)我怎么就攤上這事羽历。” “怎么了淡喜?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵窄陡,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我拆火,道長(zhǎng)跳夭,這世上最難降的妖魔是什么涂圆? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮币叹,結(jié)果婚禮上润歉,老公的妹妹穿的比我還像新娘。我一直安慰自己颈抚,他們只是感情好踩衩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著贩汉,像睡著了一般驱富。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上匹舞,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天褐鸥,我揣著相機(jī)與錄音,去河邊找鬼赐稽。 笑死叫榕,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的姊舵。 我是一名探鬼主播晰绎,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼括丁!你這毒婦竟也來(lái)了荞下?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤史飞,失蹤者是張志新(化名)和其女友劉穎尖昏,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體祸憋,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡会宪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年肖卧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蚯窥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡塞帐,死狀恐怖拦赠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情葵姥,我是刑警寧澤荷鼠,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站榔幸,受9級(jí)特大地震影響允乐,放射性物質(zhì)發(fā)生泄漏矮嫉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一牍疏、第九天 我趴在偏房一處隱蔽的房頂上張望蠢笋。 院中可真熱鬧,春花似錦鳞陨、人聲如沸昨寞。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)援岩。三九已至,卻和暖如春掏导,著一層夾襖步出監(jiān)牢的瞬間享怀,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工碘菜, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凹蜈,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓忍啸,卻偏偏與公主長(zhǎng)得像仰坦,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子计雌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

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