怎樣寫(xiě)一個(gè)解釋器-王垠

原文地址

寫(xiě)一個(gè)解釋器八匠,通常是設(shè)計(jì)和實(shí)現(xiàn)程序語(yǔ)言的第一步遭笋。解釋器是簡(jiǎn)單卻又深?yuàn)W的東西秽褒,以至于好多人都不會(huì)寫(xiě)壶硅,所以我決定寫(xiě)一篇這方面的入門讀物。

雖然我試圖從最基本的原理講起销斟,盡量不依賴于其它知識(shí)庐椒,但這并不是一本編程入門教材。我假設(shè)你已經(jīng)理解 Scheme 語(yǔ)言票堵,以及基本的編程技巧(比如遞歸)扼睬。如果你完全不了解這些逮栅,那我建議你讀一下?SICP?的第一悴势,二章,或者?HtDP?的前幾章措伐,習(xí)題可以不做特纤。注意不要讀太多書(shū),否則你就回不來(lái)了 ;-) 當(dāng)然你也可以直接讀這篇文章侥加,有不懂的地方再去查資料捧存。

實(shí)現(xiàn)語(yǔ)言容易犯的一個(gè)錯(cuò)誤,就是一開(kāi)頭就試圖去實(shí)現(xiàn)很復(fù)雜的語(yǔ)言(比如 JavaScript 或者 Python)担败。這樣你很快就會(huì)因?yàn)檫@些語(yǔ)言的復(fù)雜性昔穴,以及各種歷史遺留的設(shè)計(jì)問(wèn)題而受到挫折,最后不了了之提前。學(xué)習(xí)實(shí)現(xiàn)語(yǔ)言吗货,最好是從最簡(jiǎn)單,最干凈的語(yǔ)言開(kāi)始狈网,迅速寫(xiě)出一個(gè)可用的解釋器宙搬。之后再逐步往里面添加特性笨腥,同時(shí)保持正確。這樣你才能有條不紊地構(gòu)造出復(fù)雜的解釋器勇垛。

因?yàn)檫@個(gè)原因脖母,這篇文章只針對(duì)一個(gè)很簡(jiǎn)單的語(yǔ)言,名叫“R2”闲孤。它可以作為一個(gè)簡(jiǎn)單的計(jì)算器用谆级,還具有變量定義,函數(shù)定義和調(diào)用等功能讼积。

我們的工具:Racket

本文的解釋器是用 Scheme 語(yǔ)言實(shí)現(xiàn)的哨苛。Scheme 有很多的“實(shí)現(xiàn)”,這里我用的實(shí)現(xiàn)叫做 Racket币砂,它可以在這里免費(fèi)下載建峭。為了讓程序簡(jiǎn)潔,我用了一點(diǎn)點(diǎn) Racket 的模式匹配(pattern matching)功能决摧。我對(duì) Scheme 的實(shí)現(xiàn)沒(méi)有特別的偏好亿蒸,但 Racket 方便易用,適合教學(xué)掌桩。如果你用其它的 Scheme 實(shí)現(xiàn)边锁,可能得自己做一些調(diào)整。

Racket 具有宏(macro)波岛,所以它其實(shí)可以變成很多種語(yǔ)言茅坛。如果你之前用過(guò) DrRacket,那它的“語(yǔ)言設(shè)置”可能被你改成了 R5RS 之類的则拷。所以如果下面的程序不能運(yùn)行贡蓖,你可能需要檢查一下 DrRacket 的“語(yǔ)言設(shè)置”,把 Language 設(shè)置成 “Racket”煌茬。

Racket 允許使用方括號(hào)而不只是圓括號(hào)斥铺,所以你可以寫(xiě)這樣的代碼:

(let([x1][y2])(+xy))

方括號(hào)跟圓括號(hào)可以互換,唯一的要求是方括號(hào)必須和方括號(hào)匹配坛善。通常我喜歡用方括號(hào)來(lái)表示“無(wú)動(dòng)作”的數(shù)據(jù)(比如上面的?[x 1],?[y 2])晾蜘,這樣可以跟函數(shù)調(diào)用和其它具有“動(dòng)作”的代碼,產(chǎn)生“視覺(jué)差”眠屎。這對(duì)于代碼的可讀性是一個(gè)改善剔交,因?yàn)榈教幎际菆A括號(hào)的話,確實(shí)有點(diǎn)太單調(diào)改衩,容易打瞌睡岖常。

另外,Racket 程序的最上面都需要加上像?#lang?racket?這樣的語(yǔ)言選擇標(biāo)記燎字,這樣 Racket 才可以知道你想用哪個(gè)語(yǔ)言變種腥椒。

解釋器是什么

準(zhǔn)備工作就到這里“⒄現(xiàn)在我來(lái)談一下,解釋器到底是什么笼蛛。說(shuō)白了洒放,解釋器跟計(jì)算器差不多。解釋器是一個(gè)函數(shù)滨砍,你輸入一個(gè)“表達(dá)式”往湿,它就輸出一個(gè) “值”,像這樣:

比如惋戏,你輸入表達(dá)式?'(+ 1 2)?领追,它就輸出值,整數(shù)3响逢。表達(dá)式是一種“表象”或者“符號(hào)”绒窑,而值卻更加接近“本質(zhì)”或者“意義”。我們“解釋”了符號(hào)舔亭,得到它的意義些膨,這也許就是為什么它叫做“解釋器”。

需要注意的是钦铺,表達(dá)式是一個(gè)數(shù)據(jù)結(jié)構(gòu)订雾,而不是一個(gè)字符串。我們用一種叫“S 表達(dá)式”(S-expression)的結(jié)構(gòu)來(lái)存儲(chǔ)表達(dá)式矛洞。比如表達(dá)式?'(+ 1 2)?其實(shí)是一個(gè)鏈表(list)洼哎,它里面的內(nèi)容是三個(gè)符號(hào)(symbol):+,?1?和?2,而不是字符串"(+ 1 2)"沼本。

從 S 表達(dá)式這樣的“結(jié)構(gòu)化數(shù)據(jù)”里提取信息噩峦,方便又可靠,而從字符串里提取信息擅威,麻煩而且容易出錯(cuò)壕探。Scheme(Lisp)語(yǔ)言里面大量使用結(jié)構(gòu)化數(shù)據(jù),少用字符串郊丛,這是 Lisp 系統(tǒng)比 Unix 系統(tǒng)先進(jìn)的地方之一。

從計(jì)算理論的角度講瞧筛,每個(gè)程序都是一臺(tái)機(jī)器的“描述”厉熟,而解釋器就是在“模擬”這臺(tái)機(jī)器的運(yùn)轉(zhuǎn),也就是在進(jìn)行“計(jì)算”较幌。所以從某種意義上講揍瑟,解釋器就是計(jì)算的本質(zhì)。當(dāng)然乍炉,不同的解釋器就會(huì)帶來(lái)不同的計(jì)算绢片。

CPU 也是一個(gè)解釋器滤馍,它專門解釋執(zhí)行機(jī)器語(yǔ)言。如果你深刻理解了解釋器底循,就可以從本質(zhì)上看出各種 CPU 的設(shè)計(jì)為什么是那個(gè)樣子巢株,它們有什么優(yōu)缺點(diǎn),而不只是被動(dòng)的作為它們的使用者熙涤。

抽象語(yǔ)法樹(shù)(Abstract Syntax Tree)

用 S 表達(dá)式所表示的代碼阁苞,本質(zhì)上是一種叫做“樹(shù)”(tree)的數(shù)據(jù)結(jié)構(gòu)。更具體一點(diǎn)祠挫,這叫做“抽象語(yǔ)法樹(shù)”(Abstract Syntax Tree那槽,簡(jiǎn)稱 AST)。下文為了簡(jiǎn)潔等舔,我們省略掉“抽象”兩個(gè)字骚灸,就叫它“語(yǔ)法樹(shù)”。

跟普通的樹(shù)結(jié)構(gòu)一樣慌植,語(yǔ)法樹(shù)里的節(jié)點(diǎn)逢唤,要么是一個(gè)“葉節(jié)點(diǎn)”,要么是一顆“子樹(shù)”涤浇。葉節(jié)點(diǎn)是不能再細(xì)分的“原子”鳖藕,比如數(shù)字,字符串只锭,操作符著恩,變量名。而子樹(shù)是可以再細(xì)分的“結(jié)構(gòu)”蜻展,比如算術(shù)表達(dá)式喉誊,函數(shù)定義,函數(shù)調(diào)用纵顾,等等伍茄。

舉個(gè)簡(jiǎn)單的例子,表達(dá)式?'(* (+ 1 2) (+ 3 4))施逾,就對(duì)應(yīng)如下的語(yǔ)法樹(shù)結(jié)構(gòu):

其中敷矫,*,兩個(gè)+汉额,1曹仗,2,3蠕搜,4?都是葉節(jié)點(diǎn)怎茫,而那三個(gè)紅色節(jié)點(diǎn),都表示子樹(shù)結(jié)構(gòu):'(+ 1 2)妓灌,'(+ 3 4)轨蛤,'(* (+ 1 2) (+ 3 4))蜜宪。

樹(shù)遍歷算法

在基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)課程里,我們都學(xué)過(guò)二叉樹(shù)的遍歷操作祥山,也就是所謂先序遍歷圃验,中序遍歷和后序遍歷。語(yǔ)法樹(shù)跟二叉樹(shù)枪蘑,其實(shí)沒(méi)有很大區(qū)別损谦,所以你也可以在它上面進(jìn)行遍歷。解釋器的算法岳颇,就是在語(yǔ)法樹(shù)上的一種遍歷操作照捡。由于這個(gè)淵源關(guān)系,我們先來(lái)做一個(gè)遍歷二叉樹(shù)的練習(xí)话侧。做好了之后栗精,我們就可以把這段代碼擴(kuò)展成一個(gè)解釋器。

這個(gè)練習(xí)是這樣:寫(xiě)出一個(gè)函數(shù)瞻鹏,名叫tree-sum悲立,它對(duì)二叉樹(shù)進(jìn)行“求和”,把所有節(jié)點(diǎn)里的數(shù)加在一起新博,返回它們的和薪夕。舉個(gè)例子,(tree-sum '((1 2) (3 4)))赫悄,執(zhí)行后應(yīng)該返回?10原献。注意:這是一顆二叉樹(shù),所以不會(huì)含有長(zhǎng)度超過(guò) 2 的子樹(shù)埂淮,你不需要考慮像?((1 2) (3 4 5))?這類情況姑隅。需要考慮的例子是像這樣:(1 2),(1 (2 3)),?((1 2) 3)?((1 2) (3 4))倔撞,……

(為了達(dá)到最好的學(xué)習(xí)效果讲仰,你最好試一下寫(xiě)出這個(gè)函數(shù)再繼續(xù)往下看。)

好了痪蝇,希望你得到了跟我差不多的結(jié)果鄙陡。我的代碼是這個(gè)樣子:

#langracket(definetree-sum(lambda(exp)(matchexp; 對(duì)輸入exp進(jìn)行模式匹配[(?number?x)x]; exp是一個(gè)數(shù)x嗎?如果是霹俺,那么返回這個(gè)數(shù)x[`(,e1,e2); exp是一個(gè)含有兩棵子樹(shù)的中間節(jié)點(diǎn)嗎柔吼?(let([v1(tree-sume1)]; 遞歸調(diào)用tree-sum自己,對(duì)左子樹(shù)e1求值[v2(tree-sume2)]); 遞歸調(diào)用tree-sum自己丙唧,對(duì)右子樹(shù)e2求值(+v1v2))]))); 返回左右子樹(shù)結(jié)果v1和v2的和

你可以通過(guò)以下的例子來(lái)測(cè)試它的正確性:

(tree-sum'(12));; => 3(tree-sum'(1(23)));; => 6(tree-sum'((12)3));; => 6(tree-sum'((12)(34)));; => 10

(完整的代碼和示例,可以在這里下載觅玻。)

這個(gè)算法很簡(jiǎn)單想际,我們可以把它用文字描述如下:

如果輸入?exp?是一個(gè)數(shù)培漏,那就返回這個(gè)數(shù)。

否則如果?exp?是像?(,e1 ,e2)?這樣的子樹(shù)胡本,那么分別對(duì)?e1?和?e2?遞歸調(diào)用?tree-sum牌柄,進(jìn)行求和,得到?v1?和?v2侧甫,然后返回?v1 + v2?的和珊佣。

你自己寫(xiě)出來(lái)的代碼,也許用了 if 或者 cond 語(yǔ)句來(lái)進(jìn)行分支披粟,而我的代碼里面使用的是 Racket 的模式匹配(match)咒锻。這個(gè)例子用 if 或者 cond 其實(shí)也可以,但我之后要把這代碼擴(kuò)展成一個(gè)解釋器守屉,所以提前使用了 match惑艇。這樣跟后面的代碼對(duì)比的時(shí)候,就更容易看出規(guī)律來(lái)拇泛。接下來(lái)滨巴,我就簡(jiǎn)單講一下這個(gè) match 表達(dá)式的工作原理。

模式匹配

現(xiàn)在不得不插入一點(diǎn) Racket 的技術(shù)細(xì)節(jié)俺叭,如果你已經(jīng)學(xué)會(huì)使用 Racket 的模式匹配恭取,可以跳過(guò)這一節(jié)。你也可以通過(guò)閱讀 Racket 模式匹配的文檔來(lái)代替這一節(jié)熄守。但我建議你不要讀太多文檔蜈垮,因?yàn)槲医酉氯ブ挥玫胶苌俚哪J狡ヅ涔δ埽野阉鼈兌冀忉屓缦隆?/p>

模式匹配的形式一般是這樣:

(matchx[模式結(jié)果][模式結(jié)果]......)

它先對(duì)?x?求值柠横,然后根據(jù)值的結(jié)構(gòu)來(lái)進(jìn)行分支窃款。每個(gè)分支由兩部分組成,左邊是一個(gè)模式牍氛,右邊是一個(gè)結(jié)果晨继。整個(gè) match 語(yǔ)句的語(yǔ)義是這樣:從上到下依次考慮,找到第一個(gè)可以匹配?x的值的模式搬俊,返回它右邊的結(jié)果紊扬。左邊的模式在匹配之后,可能會(huì)綁定一些變量唉擂,這些變量可以在右邊的表達(dá)式里使用餐屎。

模式匹配是一種分支語(yǔ)句,它在邏輯上就是 Scheme(Lisp) 的?cond?表達(dá)式玩祟,或者 Java 的嵌套條件語(yǔ)句?if ... else if ... else ...腹缩。然而跟條件語(yǔ)句里的“條件”不同,每條 match 語(yǔ)句左邊的模式,可以準(zhǔn)確而形象地描述數(shù)據(jù)結(jié)構(gòu)的形狀藏鹊,而且可以在匹配的同時(shí)润讥,對(duì)結(jié)構(gòu)里的成員進(jìn)行“綁定”。這樣我們可以在右邊方便的訪問(wèn)結(jié)構(gòu)成員盘寡,而不需要使用訪問(wèn)函數(shù)(accessor)或者?foo.x?這樣的屬性語(yǔ)法(attribute)楚殿。而且模式可以有嵌套的子結(jié)構(gòu),所以它能夠一次性的表示復(fù)雜的數(shù)據(jù)結(jié)構(gòu)竿痰。

舉個(gè)實(shí)在點(diǎn)的例子脆粥。我的代碼里用了這樣一個(gè) match 表達(dá)式:

(matchexp[(?number?x)x][`(,e1,e2)(let([v1(tree-sume1)][v2(tree-sume2)])(+v1v2))])

第二行里面的?'(,e1 ,e2)?是一個(gè)模式(pattern),它被用來(lái)匹配?exp?的值影涉。如果?exp?是'(1 2)变隔,那么它與'(,e1 ,e2)匹配的時(shí)候,就會(huì)把?e1?綁定到?'1常潮,把?e2?綁定到?'2弟胀。這是因?yàn)樗鼈兘Y(jié)構(gòu)相同:

`(,e1,e2)'(12)

說(shuō)白了,模式就是一個(gè)可以含有“名字”(像?e1?和?e2)的結(jié)構(gòu)喊式,像?'(,e1 ,e2)孵户。我們拿這個(gè)帶有名字的結(jié)構(gòu),去匹配實(shí)際數(shù)據(jù)岔留,像?'(1 2)夏哭。當(dāng)它們一一對(duì)應(yīng)之后,這些名字就被綁定到數(shù)據(jù)里對(duì)應(yīng)位置的值献联。

第一行的“模式”比較特殊竖配,(? number? x)?表示的,其實(shí)是一個(gè)普通的條件判斷里逆,相當(dāng)于(number? exp)进胯,如果這個(gè)條件成立,那么它把?exp?的值綁定到?x原押,這樣右邊就可以用?x?來(lái)指代?exp胁镐。對(duì)于無(wú)法細(xì)分的結(jié)構(gòu)(比如數(shù)字,布爾值)诸衔,你只能用這種方式來(lái)“匹配”盯漂。看起來(lái)有點(diǎn)奇怪笨农,不過(guò)習(xí)慣了就好了就缆。

模式匹配對(duì)解釋器和編譯器的書(shū)寫(xiě)相當(dāng)有用,因?yàn)槌绦虻恼Z(yǔ)法樹(shù)往往具有嵌套的結(jié)構(gòu)谒亦。不用模式匹配的話竭宰,往往要寫(xiě)冗長(zhǎng)空郊,復(fù)雜,不直觀的代碼羞延,才能描述出期望的結(jié)構(gòu)渣淳。而且由于結(jié)構(gòu)的嵌套比較深脾还,很容易漏掉邊界情況伴箩,造成錯(cuò)誤。模式匹配可以直觀的描述期望的結(jié)構(gòu)鄙漏,避免漏掉邊界情況嗤谚,而且可以方便的訪問(wèn)結(jié)構(gòu)成員。

由于這個(gè)原因怔蚌,很多源于 ML 的語(yǔ)言(比如 OCaml巩步,Haskell)都有模式匹配的功能。因?yàn)?ML(Meta-Language)原來(lái)設(shè)計(jì)的用途桦踊,就是用來(lái)實(shí)現(xiàn)程序語(yǔ)言的椅野。Racket 的模式匹配也是部分受了 ML 的啟發(fā),實(shí)際上它們的原理是一模一樣的籍胯。

好了竟闪,樹(shù)遍歷的練習(xí)就做到這里。然而這跟解釋器有什么關(guān)系呢杖狼?下面我們只把它改一下炼蛤,就可以得到一個(gè)簡(jiǎn)單的解釋器。

一個(gè)計(jì)算器

計(jì)算器也是一種解釋器蝶涩,只不過(guò)它只能處理算術(shù)表達(dá)式理朋。我們的下一個(gè)目標(biāo),就是寫(xiě)出一個(gè)計(jì)算器绿聘。如果你給它?'(* (+ 1 2) (+ 3 4))嗽上,它就輸出?21∠ㄈ粒可不要小看這個(gè)計(jì)算器兽愤,稍后我們把它稍加改造,就可以得到一個(gè)更多功能的解釋器鲜屏。

上面的代碼里烹看,我們利用遞歸遍歷,對(duì)樹(shù)里的數(shù)字求和洛史。那段代碼里惯殊,其實(shí)已經(jīng)隱藏了一個(gè)解釋器的框架。你觀察一下也殖,一個(gè)算術(shù)表達(dá)式?'(* (+ 1 2) (+ 3 4))土思,跟二叉樹(shù)?'((1 2) (3 4))?有什么不同务热?發(fā)現(xiàn)沒(méi)有,這個(gè)算術(shù)表達(dá)式比起二叉樹(shù)己儒,只不過(guò)在每個(gè)子樹(shù)結(jié)構(gòu)里多出了一個(gè)操作符:一個(gè)?*?和兩個(gè)?+?崎岂。它不再是一棵二叉樹(shù),而是一種更通用的樹(shù)結(jié)構(gòu)闪湾。

這點(diǎn)區(qū)別冲甘,也就帶來(lái)了二叉樹(shù)求和與解釋器算法的區(qū)別。對(duì)二叉樹(shù)進(jìn)行求和的時(shí)候途样,在每個(gè)子樹(shù)節(jié)點(diǎn)江醇,我們都做加法。而對(duì)表達(dá)式進(jìn)行解釋的時(shí)候何暇,在每一個(gè)子樹(shù)節(jié)點(diǎn)陶夜,我們不一定進(jìn)行加法。根據(jù)子樹(shù)的“操作符”不同裆站,我們可能會(huì)選擇加条辟,減,乘宏胯,除四種操作羽嫡。

好了,下面就是這個(gè)計(jì)算器的代碼胳嘲。它接受一個(gè)表達(dá)式厂僧,輸出一個(gè)數(shù)字作為結(jié)果。

#langracket; 聲明用 Racket 語(yǔ)言(definecalc(lambda(exp)(matchexp; 分支匹配:表達(dá)式的兩種情況[(?number?x)x]; 是數(shù)字了牛,直接返回[`(,op,e1,e2); 匹配提取操作符op和兩個(gè)操作數(shù)e1,e2(let([v1(calce1)]; 遞歸調(diào)用 calc 自己颜屠,得到 e1 的值[v2(calce2)]); 遞歸調(diào)用 calc 自己,得到 e2 的值(matchop; 分支匹配:操作符 op 的 4 種情況['+(+v1v2)]; 如果是加號(hào)鹰祸,輸出結(jié)果為 (+ v1 v2)['-(-v1v2)]; 如果是減號(hào)甫窟,乘號(hào),除號(hào)蛙婴,相似的處理['*(*v1v2)]['/(/v1v2)]))])))

你可以得到如下的結(jié)果:

(calc'(+12));; => 3(calc'(*23));; => 6(calc'(*(+12)(+34)));; => 21

(完整的代碼和示例粗井,可以在這里下載。)

跟之前的二叉樹(shù)求和代碼比較一下街图,你會(huì)發(fā)現(xiàn)它們驚人的相似浇衬,因?yàn)榻忉屍鞅緛?lái)就是一個(gè)樹(shù)遍歷算法。不過(guò)你發(fā)現(xiàn)它們有什么不同嗎餐济?它們的不同點(diǎn)在于:

算術(shù)表達(dá)式的模式里面耘擂,多出了一個(gè)“操作符”(op)葉節(jié)點(diǎn):(,op ,e1 ,e2)

對(duì)子樹(shù) e1 和 e2 分別求值之后,我們不是返回?(+ v1 v2)絮姆,而是根據(jù)?op?的不同醉冤,返回不同的結(jié)果:

(matchop['+(+v1v2)]['-(-v1v2)]['*(*v1v2)]['/(/v1v2)])

最后你發(fā)現(xiàn)秩霍,一個(gè)算術(shù)表達(dá)式的解釋器,不過(guò)是一個(gè)稍加擴(kuò)展的樹(shù)遍歷算法蚁阳。

R2:一個(gè)很小的程序語(yǔ)言

實(shí)現(xiàn)了一個(gè)計(jì)算器铃绒,現(xiàn)在讓我們過(guò)渡到一種更強(qiáng)大的語(yǔ)言。為了方便稱呼螺捐,我給它起了一個(gè)萌萌噠名字颠悬,叫 R2。R2 比起之前的計(jì)算器归粉,只多出四個(gè)元素椿疗,它們分別是:變量,函數(shù)糠悼,綁定,調(diào)用浅乔。再加上之前介紹的算術(shù)操作倔喂,我們就得到一個(gè)很簡(jiǎn)單的程序語(yǔ)言,它只有5種不同的構(gòu)造靖苇。用 Scheme 的語(yǔ)法席噩,這5種構(gòu)造看起來(lái)就像這樣:

變量:x

函數(shù):(lambda (x) e)

綁定:(let ([x e1]) e2)

調(diào)用:(e1 e2)

算術(shù):(? e2 e2)

(其中,? 是一個(gè)算術(shù)操作符贤壁,可以選擇?+,?-,?*,?/?其中之一)

一般程序語(yǔ)言還有很多其它構(gòu)造悼枢,可是一開(kāi)頭就試圖去實(shí)現(xiàn)所有那些,只會(huì)讓人糊涂脾拆。最好是把這少數(shù)幾個(gè)東西搞清楚馒索,確保它們正確之后,才慢慢加入其它元素名船。

這些構(gòu)造的語(yǔ)義绰上,跟 Scheme 里面的同名構(gòu)造幾乎一模一樣。如果你不清楚什么是”綁定“渠驼,那你可以把它看成是普通語(yǔ)言里的”變量聲明“蜈块。

需要注意的是,跟一般語(yǔ)言不同迷扇,我們的函數(shù)只接受一個(gè)參數(shù)百揭。這不是一個(gè)嚴(yán)重的限制,因?yàn)樵谖覀兊恼Z(yǔ)言里蜓席,函數(shù)可以被作為值傳遞器一,也就是所謂“first-class function”。所以你可以用嵌套的函數(shù)定義來(lái)表示有兩個(gè)以上參數(shù)的函數(shù)瓮床。

舉個(gè)例子盹舞,?(lambda (x) (lambda (y) (+ x y)))?是個(gè)嵌套的函數(shù)定義产镐,它也可以被看成是有兩個(gè)參數(shù)(x?和?y)的函數(shù),這個(gè)函數(shù)返回?x?和?y?的和踢步。當(dāng)這樣的函數(shù)被調(diào)用的時(shí)候癣亚,需要兩層調(diào)用,就像這樣:

(((lambda(x)(lambda(y)(+xy)))1)2);; => 3

這種做法在PL術(shù)語(yǔ)里面获印,叫做咖喱(currying)述雾。看起來(lái)啰嗦兼丰,但這樣我們的解釋器可以很簡(jiǎn)單玻孟。等我們理解了基本的解釋器,再實(shí)現(xiàn)真正的多參數(shù)函數(shù)也不遲鳍征。

另外黍翎,我們的綁定語(yǔ)法?(let ([x e1]) e2),比起 Scheme 的綁定也有一些局限艳丛。我們的 let 只能綁定一個(gè)變量匣掸,而 Scheme 可以綁定多個(gè),像這樣?(let ([x 1] [y 2]) (+ x y))氮双。這也不是一個(gè)嚴(yán)重的限制碰酝,因?yàn)槲覀兛梢詥乱稽c(diǎn),用嵌套的 let 綁定:

(let([x1])(let([y2])(+xy)))

R2 的解釋器

下面是我們今天要完成的解釋器戴差,它可以運(yùn)行一個(gè) R2 程序送爸。你可以先留意一下各部分的注釋。

#langracket;;; 以下三個(gè)定義 env0, ext-env, lookup 是對(duì)環(huán)境(environment)的基本操作:;; 空環(huán)境(defineenv0'());; 擴(kuò)展暖释。對(duì)環(huán)境 env 進(jìn)行擴(kuò)展袭厂,把 x 映射到 v,得到一個(gè)新的環(huán)境(defineext-env(lambda(xvenv)(cons`(,x.,v)env)));; 查找饭入。在環(huán)境中 env 中查找 x 的值嵌器。如果沒(méi)找到就返回 #f(definelookup(lambda(xenv)(let([p(assqxenv)])(cond[(notp)#f][else(cdrp)]))));; 閉包的數(shù)據(jù)結(jié)構(gòu)定義,包含一個(gè)函數(shù)定義 f 和它定義時(shí)所在的環(huán)境(structClosure(fenv));; 解釋器的遞歸定義(接受兩個(gè)參數(shù)谐丢,表達(dá)式 exp 和環(huán)境 env);; 共 5 種情況(變量爽航,函數(shù),綁定乾忱,調(diào)用讥珍,數(shù)字,算術(shù)表達(dá)式)(defineinterp(lambda(expenv)(matchexp; 對(duì)exp進(jìn)行模式匹配[(?symbol?x); 變量(let([v(lookupxenv)])(cond[(notv)(error"undefined variable"x)][elsev]))][(?number?x)x]; 數(shù)字[`(lambda(,x),e); 函數(shù)(Closureexpenv)][`(let([,x,e1]),e2); 綁定(let([v1(interpe1env)])(interpe2(ext-envxv1env)))][`(,e1,e2); 調(diào)用(let([v1(interpe1env)][v2(interpe2env)])(matchv1[(Closure`(lambda(,x),e)env-save)(interpe(ext-envxv2env-save))]))][`(,op,e1,e2); 算術(shù)表達(dá)式(let([v1(interpe1env)][v2(interpe2env)])(matchop['+(+v1v2)]['-(-v1v2)]['*(*v1v2)]['/(/v1v2)]))])));; 解釋器的“用戶界面”函數(shù)窄瘟。它把 interp 包裝起來(lái)衷佃,掩蓋第二個(gè)參數(shù),初始值為 env0(definer2(lambda(exp)(interpexpenv0)))

這里有一些測(cè)試?yán)樱?/p>

(r2'(+12));; => 3(r2'(*23));; => 6(r2'(*2(+34)));; => 14(r2'(*(+12)(+34)));; => 21(r2'((lambda(x)(*2x))3));; => 6(r2'(let([x2])(let([f(lambda(y)(*xy))])(f3))));; => 6(r2'(let([x2])(let([f(lambda(y)(*xy))])(let([x4])(f3)))));; => 6

(完整的代碼和示例蹄葱,可以在這里下載氏义。)

在接下來(lái)的幾節(jié)锄列,我們來(lái)仔細(xì)看看這個(gè)解釋器的各個(gè)部分。

對(duì)基本算術(shù)操作的解釋

算術(shù)操作一般都是程序里最基本的構(gòu)造惯悠,它們不能再被細(xì)分為多個(gè)步驟邻邮,所以我們先來(lái)看看對(duì)算術(shù)操作的處理。以下就是 R2 解釋器處理算術(shù)的部分克婶,它是?interp?的最后一個(gè)分支筒严。

(matchexp......[`(,op,e1,e2)(let([v1(interpe1env)]; 遞歸調(diào)用 interp 自己,得到 e1 的值[v2(interpe2env)]); 遞歸調(diào)用 interp 自己情萤,得到 e2 的值(matchop; 分支:處理操作符 op 的 4 種情況['+(+v1v2)]; 如果是加號(hào)相满,輸出結(jié)果為 (+ v1 v2)['-(-v1v2)]; 如果是減號(hào)斥杜,乘號(hào),除號(hào)乍楚,相似的處理['*(*v1v2)]['/(/v1v2)]))])

你可以看到它幾乎跟剛才寫(xiě)的計(jì)算器一模一樣痛单,不過(guò)現(xiàn)在?interp?的調(diào)用多了一個(gè)參數(shù)?env?而已德撬。這個(gè)?env?是所謂“環(huán)境”银酬,我們下面很快就講末贾。

對(duì)數(shù)字的解釋

對(duì)數(shù)字的解釋很簡(jiǎn)單,把它們?cè)獠粍?dòng)返回就可以了勋陪。

[(? number? x) x]

變量和函數(shù)

變量和函數(shù)是解釋器里最麻煩的部分,所以我們來(lái)仔細(xì)看看硫兰。

變量(variable)的產(chǎn)生诅愚,是數(shù)學(xué)史上的最大突破之一。因?yàn)樽兞靠梢员唤壎ǖ讲煌闹到儆常瑥亩购瘮?shù)的實(shí)現(xiàn)成為可能违孝。比如數(shù)學(xué)函數(shù)?f(x) = x * 2,其中?x?是一個(gè)變量泳赋,它把輸入的值傳遞到函數(shù)體?x * 2?里面雌桑。如果沒(méi)有變量,函數(shù)就不可能實(shí)現(xiàn)祖今。

對(duì)變量最基本的操作校坑,是對(duì)它的“綁定”(binding)和“取值”(evaluate)。什么是綁定呢千诬?拿上面的函數(shù)?f(x)?作為例子耍目。當(dāng)我們調(diào)用?f(1)?時(shí),函數(shù)體里面的?x?等于 1徐绑,所以?x * 2?的值是 2邪驮,而當(dāng)我們調(diào)用?f(2)?時(shí),函數(shù)體里面的?x?等于 2傲茄,所以?x * 2?的值是 4毅访。這里沮榜,兩次對(duì)?f?的調(diào)用,分別對(duì)?x?進(jìn)行了兩次綁定喻粹。第一次?x?被綁定到了 1蟆融,第二次被綁定到了 2。

你可以把“綁定”理解成這樣一個(gè)動(dòng)作磷斧,就像當(dāng)你把插頭插進(jìn)電源插座的那一瞬間振愿。插頭的插腳就是?f(x)?里面的那個(gè)?x,而?x * 2?里面的?x弛饭,則是電線的另外一端冕末。所以當(dāng)你把插頭插進(jìn)插座,電流就通過(guò)這根電線到達(dá)另外一端侣颂。如果電線導(dǎo)電性能良好档桃,兩頭的電壓應(yīng)該相等。

環(huán)境

我們的解釋器只能一步一步的做事情憔晒。比如藻肄,當(dāng)它需要求?f(1)?的值的時(shí)候,它分成兩步操作:

把?x?綁定到 1拒担,這樣函數(shù)體內(nèi)才能看見(jiàn)這個(gè)綁定嘹屯。

進(jìn)入?f?的函數(shù)體,對(duì)?x * 2?進(jìn)行求值从撼。

這就像一個(gè)人做出這兩個(gè)動(dòng)作:

把插頭插進(jìn)插座 州弟。

到電線的另外一頭,測(cè)量它的電壓低零,并且把結(jié)果乘以 2婆翔。

在第一步和第二步之間,我們?nèi)绾斡涀?x?的值呢掏婶?通過(guò)所謂“環(huán)境”啃奴!我們用環(huán)境記錄變量的值,并且把它們傳遞到變量的“可見(jiàn)區(qū)域”雄妥。變量的可見(jiàn)區(qū)域最蕾,用術(shù)語(yǔ)說(shuō)叫做“作用域”(scope)。

在我們的解釋器里茎芭,用于處理環(huán)境的代碼如下:

;; 空環(huán)境(defineenv0'());; 對(duì)環(huán)境 env 進(jìn)行擴(kuò)展揖膜,把 x 映射到 v(defineext-env(lambda(xvenv)(cons`(,x.,v)env)));; 取值。在環(huán)境中 env 中查找 x 的值(definelookup(lambda(xenv)(let([p(assqxenv)])(cond[(notp)#f][else(cdrp)]))))

這里我們用一種最簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)梅桩,Scheme 的 association list壹粟,來(lái)表示環(huán)境。Association list 看起來(lái)像這個(gè)樣子:((x . 1) (y . 2) (z . 5))。它是一個(gè)兩元組(pair)的鏈表趁仙,左邊的元素是 key洪添,右邊的元素是 value。寫(xiě)得直觀一點(diǎn)就是:

((x.1)(y.2)(z.5))

查表操作就是從頭到尾搜索雀费,如果左邊的 key 是要找的變量干奢,就返回整個(gè) pair。簡(jiǎn)單吧盏袄?效率很低忿峻,但是足夠完成我們現(xiàn)在的任務(wù)。

ext-env?函數(shù)擴(kuò)展一個(gè)環(huán)境辕羽。比如逛尚,如果原來(lái)的環(huán)境?env1?是?((y . 2) (x . 1))?那么(ext-env x 3 env1),就會(huì)返回?((x . 3) (y . 2) (x . 1))刁愿。也就是把?(x . 3)?加到env1?的最前面去绰寞。

那我們什么時(shí)候需要擴(kuò)展環(huán)境呢?當(dāng)我們進(jìn)行綁定的時(shí)候铣口。綁定可能出現(xiàn)在函數(shù)調(diào)用時(shí)滤钱,也可能出現(xiàn)在 let 綁定時(shí)。我們選擇的數(shù)據(jù)結(jié)構(gòu)脑题,使得環(huán)境自然而然的具有了作用域(scope)的特性件缸。

環(huán)境其實(shí)是一個(gè)堆棧(stack)。內(nèi)層的綁定叔遂,會(huì)出現(xiàn)在環(huán)境的最上面停团,這就是在“壓棧”掏熬。這樣我們查找變量的時(shí)候,會(huì)優(yōu)先找到最內(nèi)層定義的變量秒梅。

舉個(gè)例子:

(let([x1]); env='()旗芬。綁定x到1。(let([y2]); env='((x . 1))捆蜀。綁定y到2疮丛。(let([x3]); env='((y . 2) (x . 1))。綁定x到3辆它。(+xy)))); env='((x . 3) (y . 2) (x . 1))誊薄。查找x,得到3锰茉;查找y呢蔫,得到2。;; => 5

這段代碼會(huì)返回5飒筑。這是因?yàn)樽顑?nèi)層的綁定片吊,把?(x . 3)?放到了環(huán)境的最前面绽昏,這樣查找?x的時(shí)候,我們首先看到?(x . 3)俏脊,然后就返回值3全谤。之前放進(jìn)去的?(x . 1)?仍然存在,但是我們先看到了最上面的那個(gè)(x . 3)爷贫,所以它被忽略了认然。

這并不等于說(shuō)?(x . 1)?就可以被改寫(xiě)或者丟棄,因?yàn)樗匀皇怯杏玫穆选D阒恍枰匆粋€(gè)稍微不同的例子卷员,就知道這是怎么回事:

(let([x1]); env='()。綁定x到1卷胯。(+(let([x2]); env='((x . 1))子刮。綁定x到2。x); env='((x . 2) (x . 1))窑睁。查找x挺峡,得到2。x)); env='((x . 1))担钮。查找x橱赠,得到1。;; => 3? ? ? ? ? ? ? ; 兩個(gè)不同的x的和箫津,1+2等于3狭姨。

這個(gè)例子會(huì)返回3。它是第3行和第4行里面兩個(gè)?x?的和苏遥。由于第3行的?x?處于內(nèi)層 let 里面饼拍,那里的環(huán)境是?((x . 2) (x . 1)),所以查找?x?的值得到2田炭。第4行的?x?在內(nèi)層 let 外面师抄,但是在外層 let 里面,那里的環(huán)境是?((x . 1))教硫,所以查找?x?的值得到1叨吮。這很符合直覺(jué),因?yàn)閤?總是找到最內(nèi)層的定義瞬矩。

值得注意的是茶鉴,環(huán)境被擴(kuò)展以后,形成了一個(gè)新的環(huán)境景用,而原來(lái)的環(huán)境并沒(méi)有被改變涵叮。比如,上面的?((y . 2) (x . 1))?并沒(méi)有刪除或者修改,只不過(guò)是被“引用”到一個(gè)更大的列表里去了围肥。

這樣不對(duì)已有數(shù)據(jù)進(jìn)行修改(mutation)的數(shù)據(jù)結(jié)構(gòu)剿干,叫做“函數(shù)式數(shù)據(jù)結(jié)構(gòu)”。函數(shù)式數(shù)據(jù)結(jié)構(gòu)只生成新的數(shù)據(jù)穆刻,而不改變或者刪除老的置尔。它可能引用老的結(jié)構(gòu),然而卻不改變老的結(jié)構(gòu)氢伟。這種“不修改”(immutable)的性質(zhì)榜轿,在我們的解釋器里是很重要的,因?yàn)楫?dāng)我們擴(kuò)展一個(gè)環(huán)境朵锣,進(jìn)入遞歸谬盐,返回之后,外層的代碼必須仍然可以訪問(wèn)原來(lái)外層的環(huán)境诚些。

當(dāng)然飞傀,我們也可以用另外的,更高效的數(shù)據(jù)結(jié)構(gòu)(比如平衡樹(shù)诬烹,串接起來(lái)的哈希表)來(lái)表示環(huán)境砸烦。如果你學(xué)究一點(diǎn),甚至可以用函數(shù)來(lái)表示環(huán)境绞吁。這里為了代碼簡(jiǎn)單幢痘,我們選擇了最笨,然而正確家破,容易理解的數(shù)據(jù)結(jié)構(gòu)颜说。

對(duì)變量的解釋

了解了變量,函數(shù)和環(huán)境汰聋,我們來(lái)看看解釋器對(duì)變量的“取值”操作门粪,也就是?match?的第一種情況。

[(? symbol? x) (lookup x env)]

這就是在環(huán)境中烹困,沿著從內(nèi)向外的“作用域順序”庄拇,查找變量的值。

這里的?(? symbol? x)?是一種特殊的模式韭邓,它使用 Scheme 函數(shù)?symbol??來(lái)判斷輸入是否是一個(gè)符號(hào),如果是溶弟,就把它綁定到?x女淑,然后你就可以在右邊用?x?來(lái)指稱這個(gè)輸入。

對(duì)綁定的解釋

現(xiàn)在我們來(lái)看看對(duì) let 綁定的解釋:

[`(let([,x,e1]),e2)(let([v1(interpe1env)]); 解釋右邊表達(dá)式e1辜御,得到值v1(interpe2(ext-envxv1env)))]; 把(x . v1)擴(kuò)充到環(huán)境頂部鸭你,對(duì)e2求值

通過(guò)代碼里的注釋,你也許已經(jīng)可以理解它在做什么。我們先對(duì)表達(dá)式?e1?求值袱巨,得到?v1阁谆。然后我們把?(x . v1)?擴(kuò)充到環(huán)境里,這樣?(let ([x e1]) ...)?內(nèi)部都可以看到?x?的值愉老。然后我們使用這個(gè)擴(kuò)充后的環(huán)境场绿,遞歸調(diào)用解釋器本身,對(duì) let 的主體?e2?求值嫉入。它的返回值就是這個(gè) let 綁定的值焰盗。

Lexical Scoping 和 Dynamic Scoping

下面我們準(zhǔn)備談?wù)労瘮?shù)定義和調(diào)用。對(duì)函數(shù)的解釋是一個(gè)微妙的問(wèn)題咒林,很容易弄錯(cuò)熬拒,這是由于函數(shù)體內(nèi)也許會(huì)含有外層的變量,叫做“自由變量”垫竞。所以在分析函數(shù)的代碼之前澎粟,我們來(lái)了解一下不同的“作用域”(scoping)規(guī)則。

我們舉個(gè)例子來(lái)解釋這個(gè)問(wèn)題欢瞪。下面這段代碼活烙,它的值應(yīng)該是多少呢?

(let([x2])(let([f(lambda(y)(*xy))])(let([x4])(f3))))

在這里引有,f?函數(shù)體?(lambda (y) (* x y))?里的那個(gè)?x瓣颅,就是一個(gè)“自由變量”。x?并不是這個(gè)函數(shù)的參數(shù)譬正,也不是在這個(gè)函數(shù)里面定義的宫补,所以我們必須到函數(shù)外面去找?x?的值。

我們的代碼里面曾我,有兩個(gè)地方對(duì)?x?進(jìn)行了綁定粉怕,一個(gè)等于2,一個(gè)等于4抒巢,那么?x?到底應(yīng)該是指向哪一個(gè)綁定呢贫贝?這似乎無(wú)關(guān)痛癢,然而當(dāng)我們調(diào)用?(f 3)?的時(shí)候蛉谜,嚴(yán)重的問(wèn)題來(lái)了稚晚。f的函數(shù)體是?(* x y),我們知道?y?的值來(lái)自參數(shù) 3型诚,可是?x?的值是多少呢客燕?它應(yīng)該是2,還是4呢狰贯?

在歷史上也搓,這段代碼可能有兩種不同的結(jié)果赏廓,這種區(qū)別一直延續(xù)到今天。如果你在 Scheme (Racket)里面寫(xiě)以上的代碼傍妒,它的結(jié)果是6幔摸。

;; Scheme(let([x2])(let([f(lambda(y)(*xy))])(let([x4])(f3))));; => 6

現(xiàn)在我們來(lái)看看,在 Emacs Lisp 里面輸入等價(jià)的代碼颤练,得到什么結(jié)果。如果你不熟悉 Emacs Lisp 的用法昔案,那你可以跟我做:把代碼輸入 Emacs 的那個(gè)叫?*scratch*?的 buffer。把光標(biāo)放在代碼最后庆亡,然后按 C-x C-e捞稿,這樣 Emacs 會(huì)執(zhí)行這段代碼,然后在 minibuffer 里顯示結(jié)果:

結(jié)果是12彰亥!如果你把代碼最內(nèi)層的?x?綁定修成其它的值衰齐,輸出會(huì)隨之改變。

奇怪吧废酷?Scheme 和 Emacs Lisp抹缕,到底有什么不一樣呢澈蟆?實(shí)際上,這兩種看似差不多的 “Lisp 方言”卓研,采用了兩種完全不同的作用域方式趴俘。Scheme 的方式叫做 lexical scoping (或者 static scoping),而 Emacs 的方式叫做 dynamic scoping奏赘。

那么哪一種方式更好呢寥闪?或者用哪一種都無(wú)所謂?答案是磨淌,dynamic scoping 是非常錯(cuò)誤的做法橙垢。歷史的教訓(xùn)告訴我們,它會(huì)帶來(lái)許許多多莫名其妙的 bug伦糯,導(dǎo)致 dynamic scoping 的語(yǔ)言幾乎完全沒(méi)法用柜某。這是為什么呢?

原因在于敛纲,像?(let ((x 4)) …)?這樣的變量綁定喂击,只應(yīng)該影響它內(nèi)部“看得見(jiàn)”的?x?的值。當(dāng)我們看見(jiàn)?(let ((x 4)) (f 3))?的時(shí)候淤翔,并沒(méi)有在 let 的內(nèi)部看見(jiàn)任何叫“x” 的變量翰绊,所以我們“直覺(jué)”的認(rèn)為,(let ((x 4)) …)?對(duì)?x?的綁定旁壮,不應(yīng)該引起?(f 3)?的結(jié)果變化监嗜。

然而對(duì)于 dynamic scoping,我們的直覺(jué)卻是錯(cuò)誤的抡谐。因?yàn)?f?的函數(shù)體里面有一個(gè)?x裁奇,雖然我們沒(méi)有在?(f 3)?這個(gè)調(diào)用里面看見(jiàn)它,然而它卻存在于?f?定義的地方麦撵。要知道刽肠,f?定義的地方也許隔著幾百行代碼,甚至在另外一個(gè)文件里面躺涝。而且調(diào)用函數(shù)的人憑什么應(yīng)該知道坚嗜,?f的定義里面有一個(gè)自由變量,它的名字叫做?x银室?所以 dynamic scoping 在設(shè)計(jì)學(xué)的角度來(lái)看蜈敢,是一個(gè)反人類的設(shè)計(jì) :)

相反,lexical scoping 卻是符合人們直覺(jué)的否过。雖然在?(let ((x 4)) (f 3))?里面,我們把x?綁定到了 4癌佩,然而?f?的函數(shù)體并不是在那里定義的围辙,我們也沒(méi)在那里看見(jiàn)任何?x,所以?f?的函數(shù)體里面的?x桥胞,仍然指向我們定義它的時(shí)候看得見(jiàn)的那個(gè)?x贩虾,也就是最上面的那個(gè)?(let ([x 2]) ...),它的值是 2策精。所以?(f 3)?的值應(yīng)該等于 6,而不是12询刹。

對(duì)函數(shù)的解釋

為了實(shí)現(xiàn) lexical scoping凹联,我們必須把函數(shù)做成“閉包”(closure)。閉包是一種特殊的數(shù)據(jù)結(jié)構(gòu)澳淑,它由兩個(gè)元素組成:函數(shù)的定義和當(dāng)前的環(huán)境杠巡。我們把閉包定義為一個(gè) Racket 的 struct 結(jié)構(gòu):

(structClosure(fenv))

有了這個(gè)數(shù)據(jù)結(jié)構(gòu),我們對(duì)?(lambda (x) e)?的解釋就可以寫(xiě)成這樣:

[`(lambda(,x),e)(Closureexpenv)]

注意這里的?exp?就是 ``(lambda (,x) ,e)` 自己兄一。

有意思的是,我們的解釋器遇到?(lambda (x) e)骂束,幾乎沒(méi)有做任何計(jì)算展箱。它只是把這個(gè)函數(shù)包裝了一下,把它與當(dāng)前的環(huán)境一起栖榨,打包放到一個(gè)數(shù)據(jù)結(jié)構(gòu)(Closure)里面婴栽。這個(gè)閉包結(jié)構(gòu),記錄了我們?cè)诤瘮?shù)定義的位置“看得見(jiàn)”的那個(gè)環(huán)境准脂。稍候在調(diào)用的時(shí)候狸膏,我們就能從這個(gè)閉包的環(huán)境里面贤旷,得到函數(shù)體內(nèi)的自由變量的值。

對(duì)調(diào)用的解釋

好了盅藻,我們終于到了最后的關(guān)頭氏淑,函數(shù)調(diào)用。為了直觀炉擅,我們把函數(shù)調(diào)用的代碼拷貝如下:

[`(,e1,e2)(let([v1(interpe1env)]; 計(jì)算函數(shù) e1 的值[v2(interpe2env)]); 計(jì)算參數(shù) e2 的值(matchv1[(Closure`(lambda(,x),e)env-save); 用模式匹配的方式取出閉包里的各個(gè)子結(jié)構(gòu)(interpe(ext-envxv2env-save))]))]; 在閉包的環(huán)境env-save中把x綁定到v2眶俩,解釋函數(shù)體? ?

函數(shù)調(diào)用都是?(e1 e2)?這樣的形式仿便,e1?表示函數(shù),e2?是它的參數(shù)闻坚。我們需要先分別求出函數(shù)?e1?和參數(shù)?e2?的值窿凤。

函數(shù)調(diào)用就像把一個(gè)電器的插頭插進(jìn)插座雳殊,使它開(kāi)始運(yùn)轉(zhuǎn)。比如仓洼,當(dāng)?(lambda (x) (* x 2))?被作用于 1 時(shí)哺呜,我們把?x?綁定到 1某残,然后解釋它的函數(shù)體?(* x 2)。但是這里有一個(gè)問(wèn)題,函數(shù)體內(nèi)的自由變量應(yīng)該取什么值呢耻瑟?從上面閉包的討論谆构,你已經(jīng)知道了搬素,自由變量的值,應(yīng)該從閉包的環(huán)境查詢粱哼。

操作數(shù)?e1?的值?v1?是一個(gè)閉包,它里面包含一個(gè)函數(shù)定義時(shí)保存的環(huán)境?env-save刻蚯。我們把這個(gè)環(huán)境?env-save?取出來(lái)躬充,那我們就可以查詢它口蝠,得到函數(shù)體內(nèi)自由變量的值妙蔗。然而函數(shù)體內(nèi)不僅有自由變量眉反,還有對(duì)函數(shù)參數(shù)的使用,所以我們必須擴(kuò)展這個(gè)?env-save?環(huán)境梳杏,把參數(shù)的值加進(jìn)去。這就是為什么我們使用?(ext-env x v2 env-save)塑悼,而不只是?env-save霞势。

你可能會(huì)奇怪,那么解釋器的環(huán)境?env?難道這里就不用了嗎颂鸿?是的。我們通過(guò)?env?來(lái)計(jì)算?e1和?e2?的值栽渴,是因?yàn)?e1?和?e2?里面的變量慢味,在“當(dāng)前環(huán)境”(env)里面看得見(jiàn)〕刍#可是函數(shù)體的定義叫编,在當(dāng)前環(huán)境下是看不見(jiàn)的。它的代碼在別的地方霞篡,而那個(gè)地方看得見(jiàn)的環(huán)境,被我們存在閉包里了端逼,它就是?env-save寇损。所以我們把?v1?里面的閉包環(huán)境?env-save?取出來(lái),用于計(jì)算函數(shù)體的值裳食。

有意思的是,如果我們用?env芙沥,而不是env-save?來(lái)解釋函數(shù)體诲祸,那我們的語(yǔ)言就變成了 dynamic scoping。現(xiàn)在來(lái)實(shí)驗(yàn)一下:你可以把?(interp e (ext-env x v2 env-save))里面的?env-save?改成?env救氯,再試試我們之前討論過(guò)的代碼甲抖,它的輸出就會(huì)變成 12。那就是我們之前講過(guò)的,dynamic scoping 的結(jié)果澳盐。

(r2'(let([x2])(let([f(lambda(y)(*xy))])(let([x4])(f3)))));; => 12

你也許發(fā)現(xiàn)了,如果我們的語(yǔ)言是 dynamic scoping,那就沒(méi)必要使用閉包了源武,因?yàn)槲覀兏静恍枰]包里面保存的環(huán)境。這樣一來(lái),dynamic scoping 的解釋器就可以寫(xiě)成這樣:

(defineinterp(lambda(expenv)(matchexp......[`(lambda(,x),e); 函數(shù):直接返回自己的表達(dá)式exp]......[`(,e1,e2)(let([v1(interpe1env)][v2(interpe2env)])(matchv1[`(lambda(,x),e); 調(diào)用:直接使用函數(shù)的表達(dá)式本身(interpe(ext-envxv2env))]))]......)))

注意到這個(gè)解釋器的函數(shù)有多容易實(shí)現(xiàn)嗎露戒?它就是這個(gè)函數(shù)的表達(dá)式自己旱眯,原封不動(dòng)。用函數(shù)的表達(dá)式本身來(lái)表示它的值丸氛,是很直接很簡(jiǎn)單的做法,也是大部分人一開(kāi)頭就會(huì)想到的旦签。然而這樣實(shí)現(xiàn)出來(lái)的語(yǔ)言罩阵,就不知不覺(jué)地采用了 dynamic scoping喧笔。

這就是為什么很多早期的 Lisp 語(yǔ)言日丹,比如 Emacs Lisp,都使用 dynamic scoping迄本。這并不是因?yàn)樗鼈兊脑O(shè)計(jì)者在 dynamic scoping 和 lexical scoping 兩者之中做出了選擇抓韩,而是因?yàn)槭褂煤瘮?shù)的表達(dá)式本身來(lái)作為它的值,是最直接,一般人都會(huì)首先想到的做法。

另外涛癌,在這里我們也看到環(huán)境用“函數(shù)式數(shù)據(jù)結(jié)構(gòu)”表示的好處。閉包被調(diào)用時(shí)它的環(huán)境被擴(kuò)展降允,但是這并不會(huì)影響原來(lái)的那個(gè)環(huán)境,我們得到的是一個(gè)新的環(huán)境褂微。所以當(dāng)函數(shù)調(diào)用返回之后著隆,函數(shù)的參數(shù)綁定就自動(dòng)“注銷”了流酬。

如果你用一個(gè)非函數(shù)式的數(shù)據(jù)結(jié)構(gòu),在綁定參數(shù)時(shí)不生成新的環(huán)境拘泞,而是對(duì)已有環(huán)境進(jìn)行賦值纷纫,那么這個(gè)賦值操作就會(huì)永久性的改變?cè)瓉?lái)環(huán)境的內(nèi)容。所以你在函數(shù)返回之后必須刪除參數(shù)的綁定陪腌。這樣不但麻煩辱魁,而且在復(fù)雜的情況下很容易出錯(cuò)。

思考題:可能有些人看過(guò) lambda calculus诗鸭,這些人可能知道?(let ([x e1]) e2)?其實(shí)等價(jià)于一個(gè)函數(shù)調(diào)用:((lambda (x) e2) e1)∪敬兀現(xiàn)在問(wèn)題來(lái)了,我們?cè)谟懻摵瘮?shù)和調(diào)用的時(shí)候强岸,很深入的討論了關(guān)于 lexical scoping 和 dynamic scoping 的差別锻弓。既然 let 綁定等價(jià)于一個(gè)函數(shù)定義和調(diào)用,為什么之前我們討論對(duì)綁定的時(shí)候蝌箍,沒(méi)有討論過(guò) lexical scoping 和 dynamic scoping 的問(wèn)題青灼,也沒(méi)有制造過(guò)閉包呢?

不足之處

現(xiàn)在你已經(jīng)學(xué)會(huì)了如何寫(xiě)出一個(gè)簡(jiǎn)單的解釋器妓盲,它可以處理一個(gè)相當(dāng)強(qiáng)大的函數(shù)式語(yǔ)言杂拨。出于教學(xué)的考慮,這個(gè)解釋器并沒(méi)有考慮實(shí)用的需求悯衬,所以它并不能作為工業(yè)應(yīng)用弹沽。在這里,我指出它的一些不足之處。

缺少必要的語(yǔ)言構(gòu)造贷币。我們的語(yǔ)言里缺少好些實(shí)用語(yǔ)言必須的構(gòu)造:遞歸,數(shù)組亏狰,賦值操作役纹,字符串,自定義數(shù)據(jù)結(jié)構(gòu)暇唾,…… 作為一篇基礎(chǔ)性的讀物促脉,我不能把這些都加進(jìn)來(lái)。如果你對(duì)這些有興趣策州,可以看看其它書(shū)籍瘸味,或者等待我的后續(xù)作品。

不合法代碼的檢測(cè)和報(bào)告够挂。你也許發(fā)現(xiàn)了旁仿,這個(gè)解釋器的 match 表達(dá)式,全都假定了輸入都是合法的程序孽糖,它并沒(méi)有檢查不合法的情況枯冈。如果你給它一個(gè)不合法的程序,它不會(huì)馬上報(bào)錯(cuò)办悟,而是會(huì)真去算它尘奏,以至于導(dǎo)致奇怪的后果。一個(gè)實(shí)用的解釋器病蛉,必須加入對(duì)代碼格式進(jìn)行全面檢測(cè)炫加,在運(yùn)行之前就報(bào)告不合法的代碼結(jié)構(gòu)。

低效率的數(shù)據(jù)結(jié)構(gòu)铺然。在 association list 里面查找變量俗孝,是線性的復(fù)雜度。當(dāng)程序有很多變量的時(shí)候就有性能問(wèn)題魄健。一個(gè)實(shí)用的解釋器驹针,需要更高效的數(shù)據(jù)結(jié)構(gòu)。這種數(shù)據(jù)結(jié)構(gòu)不一定非得是函數(shù)式的诀艰。你也可以用非函數(shù)式的數(shù)據(jù)結(jié)構(gòu)(比如哈希表)柬甥,經(jīng)過(guò)一定的改造,達(dá)到同樣的性質(zhì)其垄,卻具有更高的效率苛蒲。 ? 另外,你還可以把環(huán)境轉(zhuǎn)化成一個(gè)數(shù)組绿满。給環(huán)境里的每個(gè)變量分配一個(gè)下標(biāo)(index)臂外,在這個(gè)數(shù)組里就可以找到它的值。如果你用數(shù)組表示環(huán)境,那么這個(gè)解釋器就向編譯器邁進(jìn)了一步漏健。

S 表達(dá)式的歧義問(wèn)題嚎货。為了教學(xué)需要,我們的解釋器直接使用 S 表達(dá)式來(lái)表達(dá)語(yǔ)法樹(shù)蔫浆,用模式匹配來(lái)進(jìn)行分支遍歷殖属。在實(shí)際的語(yǔ)言里,這種方式會(huì)帶來(lái)比較大的問(wèn)題瓦盛。因?yàn)?S 表達(dá)式是一種通用的數(shù)據(jù)結(jié)構(gòu)洗显,用它表示的東西,看起來(lái)都差不多的樣子原环。一旦程序的語(yǔ)法構(gòu)造多起來(lái)挠唆,直接對(duì) S 表達(dá)式進(jìn)行模式匹配,會(huì)造成歧義嘱吗。 ?

比如?(,op ,e1 ,e2)?玄组,你以為它只匹配二元算術(shù)操作,比如?(+ 1 2)谒麦。但它其實(shí)也可以匹配一個(gè) let 綁定:?(let ([x 1]) (* x 2))巧勤。這是因?yàn)樗鼈冺攲釉氐臄?shù)目是一樣的。為了消除歧義弄匕,你得小心的安排模式的順序颅悉,比如你必須把?(let ([,x ,e1]) ,e2)?的模式放在?(,op ,e1, e2)?前面。所以最好的辦法迁匠,是不要直接在 S 表達(dá)式上寫(xiě)解釋器剩瓶,而是先寫(xiě)一個(gè)“parser”,這個(gè) parser 把 S 表達(dá)式轉(zhuǎn)換成 Racket 的 struct 結(jié)構(gòu)城丧。然后解釋器再在 struct 上面進(jìn)行分支匹配延曙。這樣解釋器不用擔(dān)心歧義問(wèn)題,而且會(huì)帶來(lái)效率的提升亡哄。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末枝缔,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蚊惯,更是在濱河造成了極大的恐慌愿卸,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件截型,死亡現(xiàn)場(chǎng)離奇詭異趴荸,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)宦焦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門发钝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)顿涣,“玉大人,你說(shuō)我怎么就攤上這事酝豪√伪” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵孵淘,是天一觀的道長(zhǎng)蒲障。 經(jīng)常有香客問(wèn)我,道長(zhǎng)夺英,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任滋捶,我火速辦了婚禮痛悯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘重窟。我一直安慰自己载萌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布巡扇。 她就那樣靜靜地躺著扭仁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪厅翔。 梳的紋絲不亂的頭發(fā)上乖坠,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音刀闷,去河邊找鬼熊泵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛甸昏,可吹牛的內(nèi)容都是我干的顽分。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼施蜜,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼卒蘸!你這毒婦竟也來(lái)了翻默?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤和泌,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后祠肥,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體梯皿,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡县恕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年东羹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忠烛。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡美尸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出师坎,到底是詐尸還是另有隱情胯陋,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布义矛,位于F島的核電站盟萨,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏噪矛。R本人自食惡果不足惜铺罢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一韭赘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧脉漏,春花似錦、人聲如沸袖牙。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鳍烁。三九已至繁扎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間爹梁,已是汗流浹背提澎。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工虱朵, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钓账,地道東北人梆暮。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像偿荷,于是被迫代替她去往敵國(guó)和親唠椭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子贪嫂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

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