怎样写一个解释器
轉(zhuǎn)載: 怎樣寫一個(gè)解釋器
寫一個(gè)解釋器,通常是設(shè)計(jì)和實(shí)現(xiàn)程序語言的第一步。解釋器是簡單卻又深?yuàn)W的東西,以至于好多人都不會(huì)寫,所以我決定寫一篇這方面的入門讀物。
雖然我試圖從最基本的原理講起,盡量不依賴于其它知識(shí),但這并不是一本編程入門教材。我假設(shè)你已經(jīng)理解 Scheme 語言,以及基本的編程技巧(比如遞歸)。如果你完全不了解這些,那我建議你讀一下 SICP 的第一,二章,或者 HtDP 的前幾章,習(xí)題可以不做。注意不要讀太多書,否則你就回不來了 ;-) 當(dāng)然你也可以直接讀這篇文章,有不懂的地方再去查資料。
實(shí)現(xiàn)語言容易犯的一個(gè)錯(cuò)誤,就是一開頭就試圖去實(shí)現(xiàn)很復(fù)雜的語言(比如 JavaScript 或者 Python)。這樣你很快就會(huì)因?yàn)檫@些語言的復(fù)雜性,以及各種歷史遺留的設(shè)計(jì)問題而受到挫折,最后不了了之。學(xué)習(xí)實(shí)現(xiàn)語言,最好是從最簡單,最干凈的語言開始,迅速寫出一個(gè)可用的解釋器。之后再逐步往里面添加特性,同時(shí)保持正確。這樣你才能有條不紊地構(gòu)造出復(fù)雜的解釋器。
因?yàn)檫@個(gè)原因,這篇文章只針對(duì)一個(gè)很簡單的語言,名叫“R2”。它可以作為一個(gè)簡單的計(jì)算器用,還具有變量定義,函數(shù)定義和調(diào)用等功能。
我們的工具:Racket
本文的解釋器是用 Scheme 語言實(shí)現(xiàn)的。Scheme 有很多的“實(shí)現(xiàn)”,這里我用的實(shí)現(xiàn)叫做 Racket,它可以在這里免費(fèi)下載。為了讓程序簡潔,我用了一點(diǎn)點(diǎn) Racket 的模式匹配(pattern matching)功能。我對(duì) Scheme 的實(shí)現(xiàn)沒有特別的偏好,但 Racket 方便易用,適合教學(xué)。如果你用其它的 Scheme 實(shí)現(xiàn),可能得自己做一些調(diào)整。
Racket 具有宏(macro),所以它其實(shí)可以變成很多種語言。如果你之前用過 DrRacket,那它的“語言設(shè)置”可能被你改成了 R5RS 之類的。所以如果下面的程序不能運(yùn)行,你可能需要檢查一下 DrRacket 的“語言設(shè)置”,把 Language 設(shè)置成 “Racket”。
Racket 允許使用方括號(hào)而不只是圓括號(hào),所以你可以寫這樣的代碼:
方括號(hào)跟圓括號(hào)可以互換,唯一的要求是方括號(hào)必須和方括號(hào)匹配。通常我喜歡用方括號(hào)來表示“無動(dòng)作”的數(shù)據(jù)(比如上面的 [x 1], [y 2]),這樣可以跟函數(shù)調(diào)用和其它具有“動(dòng)作”的代碼,產(chǎn)生“視覺差”。這對(duì)于代碼的可讀性是一個(gè)改善,因?yàn)榈教幎际菆A括號(hào)的話,確實(shí)有點(diǎn)太單調(diào)。
另外,Racket 程序的最上面都需要加上像 #lang racket 這樣的語言選擇標(biāo)記,這樣 Racket 才可以知道你想用哪個(gè)語言變種。
解釋器是什么
準(zhǔn)備工作就到這里。現(xiàn)在我來談一下,解釋器到底是什么。說白了,解釋器跟計(jì)算器差不多。解釋器是一個(gè)函數(shù),你輸入一個(gè)“表達(dá)式”,它就輸出一個(gè) “值”,像這樣:
比如,你輸入表達(dá)式 ‘(+ 1 2) ,它就輸出值,整數(shù)3。表達(dá)式是一種“表象”或者“符號(hào)”,而值卻更加接近“本質(zhì)”或者“意義”。解釋器從符號(hào)出發(fā),得到它的意義,這也許就是它為什么叫做“解釋器”。
需要注意的是,表達(dá)式是一個(gè)數(shù)據(jù)結(jié)構(gòu),而不是一個(gè)字符串。我們用一種叫“S表達(dá)式”(S-expression)的結(jié)構(gòu)來存儲(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)語言里面大量使用結(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ì)帶來不同的計(jì)算。你可能沒有想到,CPU 也是一個(gè)解釋器,它專門解釋執(zhí)行機(jī)器語言。
抽象語法樹(Abstract Syntax Tree)
我們用S表達(dá)式所表示的代碼,本質(zhì)上是一種叫做“樹”(tree)的數(shù)據(jù)結(jié)構(gòu)。更具體一點(diǎn),這叫做“抽象語法樹”(Abstract Syntax Tree,簡稱 AST)。下文為了簡潔,我們省略掉“抽象”兩個(gè)字,就叫它“語法樹”。
跟普通的樹結(jié)構(gòu)一樣,語法樹里的節(jié)點(diǎn),要么是一個(gè)“葉節(jié)點(diǎn)”,要么是一顆“子樹”。葉節(jié)點(diǎn)是不能再細(xì)分的“原子”,比如數(shù)字,字符串,操作符,變量名。而子樹是可以再細(xì)分的“結(jié)構(gòu)”,比如算術(shù)表達(dá)式,函數(shù)定義,函數(shù)調(diào)用,等等。
舉個(gè)簡單的例子,表達(dá)式 ‘(* (+ 1 2) (+ 3 4)),就對(duì)應(yīng)如下的語法樹結(jié)構(gòu):
其中,,兩個(gè)+,1,2,3,4 都是葉節(jié)點(diǎn),而那三個(gè)紅色節(jié)點(diǎn),都表示子樹結(jié)構(gòu):’(+ 1 2),’(+ 3 4),’( (+ 1 2) (+ 3 4))。
樹遍歷算法
在基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)課程里,我們都學(xué)過二叉樹的遍歷操作,也就是所謂先序遍歷,中序遍歷和后序遍歷。語法樹跟二叉樹,其實(shí)沒有很大區(qū)別,所以你也可以在它上面進(jìn)行遍歷。解釋器的算法,就是在語法樹上的一種遍歷操作。由于這個(gè)淵源關(guān)系,我們先來做一個(gè)遍歷二叉樹的練習(xí)。做好了之后,我們就可以把這段代碼擴(kuò)展成一個(gè)解釋器。
這個(gè)練習(xí)是這樣:寫出一個(gè)函數(shù),名叫tree-sum,它對(duì)二叉樹進(jìn)行“求和”,把所有節(jié)點(diǎn)里的數(shù)加在一起,返回它們的和。舉個(gè)例子,(tree-sum ‘((1 2) (3 4))),執(zhí)行后應(yīng)該返回 10。注意:這是一顆二叉樹,所以不會(huì)含有長度超過2的子樹,你不需要考慮像 ((1 2) (3 4 5)) 這類情況。需要考慮的例子是像這樣:(1 2),(1 (2 3)), ((1 2) 3) ((1 2) (3 4)),……
(為了達(dá)到最好的學(xué)習(xí)效果,你最好試一下寫出這個(gè)函數(shù)再繼續(xù)往下看。)
好了,希望你得到了跟我差不多的結(jié)果。我的代碼是這個(gè)樣子:
#lang racket(define tree-sum(lambda (exp)(match exp ; 對(duì)輸入exp進(jìn)行模式匹配[(? number? x) x] ; exp是一個(gè)數(shù)x嗎?如果是,那么返回這個(gè)數(shù)x[`(,e1 ,e2) ; exp是一個(gè)含有兩棵子樹的中間節(jié)點(diǎn)嗎?(let ([v1 (tree-sum e1)] ; 遞歸調(diào)用tree-sum自己,對(duì)左子樹e1求值 [v2 (tree-sum e2)]) ; 遞歸調(diào)用tree-sum自己,對(duì)右子樹e2求值(+ v1 v2))]))) ; 返回左右子樹結(jié)果v1和v2的和你可以通過以下的例子來測(cè)試它的正確性:
(tree-sum '(1 2)) ;; => 3 (tree-sum '(1 (2 3))) ;; => 6 (tree-sum '((1 2) 3)) ;; => 6 (tree-sum '((1 2) (3 4))) ;; => 10這個(gè)算法很簡單,我們可以把它用文字描述如下:
如果輸入 exp 是一個(gè)數(shù),那就返回這個(gè)數(shù)。
否則如果 exp 是像 (,e1 ,e2) 這樣的子樹,那么分別對(duì) e1 和 e2 遞歸調(diào)用 tree-sum,進(jìn)行求和,得到 v1 和 v2,然后返回 v1 + v2 的和。
你自己寫出來的代碼,也許用了 if 或者 cond 語句來進(jìn)行分支,而我的代碼里面使用的是 Racket 的模式匹配(match)。這個(gè)例子用 if 或者 cond 其實(shí)也可以,但我之后要把這代碼擴(kuò)展成一個(gè)解釋器,所以提前使用了 match。這樣跟后面的代碼對(duì)比的時(shí)候,就更容易看出規(guī)律來。接下來,我就簡單講一下這個(gè) match 表達(dá)式的工作原理。
模式匹配
現(xiàn)在不得不插入一點(diǎn) Racket 的技術(shù)細(xì)節(jié),如果你已經(jīng)學(xué)會(huì)使用 Racket 的模式匹配,可以跳過這一節(jié)。你也可以通過閱讀 Racket 模式匹配的文檔來代替這一節(jié)。但我建議你不要讀太多文檔,因?yàn)槲医酉氯ブ挥玫胶苌俚哪J狡ヅ涔δ?#xff0c;我把它們都解釋如下。
模式匹配的形式一般是這樣:
(match x[模式 結(jié)果][模式 結(jié)果]... ... )它先對(duì) x 求值,然后根據(jù)值的結(jié)構(gòu)來進(jìn)行分支。每個(gè)分支由兩部分組成,左邊是一個(gè)模式,右邊是一個(gè)結(jié)果。整個(gè) match 語句的語義是這樣:從上到下依次考慮,找到第一個(gè)可以匹配 x 的值的模式,返回它右邊的結(jié)果。左邊的模式在匹配之后,可能會(huì)綁定一些變量,這些變量可以在右邊的表達(dá)式里使用。
模式匹配是一種分支語句,它在邏輯上就是 Scheme(Lisp) 的 cond 表達(dá)式,或者 Java 的嵌套條件語句 if … else if … else …。然而跟條件語句里的“條件”不同,每條 match 語句左邊的模式,可以準(zhǔn)確而形象地描述數(shù)據(jù)結(jié)構(gòu)的形狀,而且可以在匹配的同時(shí),對(duì)結(jié)構(gòu)里的成員進(jìn)行“綁定”。這樣我們可以在右邊方便的訪問結(jié)構(gòu)成員,而不需要使用訪問函數(shù)(accessor)或者 foo.x 這樣的屬性語法(attribute)。而且模式可以有嵌套的子結(jié)構(gòu),所以它能夠一次性的表示復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。
舉個(gè)實(shí)在點(diǎn)的例子。我的代碼里用了這樣一個(gè) match 表達(dá)式:
(match exp[(? number? x) x][`(,e1 ,e2)(let ([v1 (tree-sum e1)][v2 (tree-sum e2)])(+ v1 v2))])第二行里面的 ‘(,e1 ,e2) 是一個(gè)模式(pattern),它被用來匹配 exp 的值。如果 exp 是 ‘(1 2),那么它與’(,e1 ,e2)匹配的時(shí)候,就會(huì)把 e1 綁定到 ‘1,把 e2 綁定到 ‘2。這是因?yàn)樗鼈兘Y(jié)構(gòu)相同:
`(,e1 ,e2) '( 1 2)說白了,模式就是一個(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 來指代 exp。對(duì)于無法細(xì)分的結(jié)構(gòu)(比如數(shù)字,布爾值),你只能用這種方式來“匹配”。看起來有點(diǎn)奇怪,不過習(xí)慣了就好了。
模式匹配對(duì)解釋器和編譯器的書寫相當(dāng)有用,因?yàn)槌绦虻恼Z法樹往往具有嵌套的結(jié)構(gòu)。不用模式匹配的話,往往要寫冗長,復(fù)雜,不直觀的代碼,才能描述出期望的結(jié)構(gòu)。而且由于結(jié)構(gòu)的嵌套比較深,很容易漏掉邊界情況,造成錯(cuò)誤。模式匹配可以直觀的描述期望的結(jié)構(gòu),避免漏掉邊界情況,而且可以方便的訪問結(jié)構(gòu)成員。
由于這個(gè)原因,很多源于 ML 的語言(比如 OCaml,Haskell)都有模式匹配的功能。因?yàn)?ML(Meta-Language)原來設(shè)計(jì)的用途,就是用來實(shí)現(xiàn)程序語言的。Racket 的模式匹配也是部分受了 ML 的啟發(fā),實(shí)際上它們的原理是一模一樣的。
好了,樹遍歷的練習(xí)就做到這里。然而這跟解釋器有什么關(guān)系呢?下面我們只把它改一下,就可以得到一個(gè)簡單的解釋器。
一個(gè)計(jì)算器
計(jì)算器也是一種解釋器,只不過它只能處理算術(shù)表達(dá)式。我們的下一個(gè)目標(biāo),就是寫出一個(gè)計(jì)算器。如果你給它 ‘(* (+ 1 2) (+ 3 4)),它就輸出 21。可不要小看這個(gè)計(jì)算器,稍后我們把它稍加改造,就可以得到一個(gè)更多功能的解釋器。
上面的代碼里,我們利用遞歸遍歷,對(duì)樹里的數(shù)字求和。那段代碼里,其實(shí)已經(jīng)隱藏了一個(gè)解釋器的框架。你觀察一下,一個(gè)算術(shù)表達(dá)式 ‘(* (+ 1 2) (+ 3 4)),跟二叉樹 ‘((1 2) (3 4)) 有什么不同?發(fā)現(xiàn)沒有,這個(gè)算術(shù)表達(dá)式比起二叉樹,只不過在每個(gè)子樹結(jié)構(gòu)里多出了一個(gè)操作符:一個(gè) * 和兩個(gè) + 。它不再是一棵二叉樹,而是一種更通用的樹結(jié)構(gòu)。
這點(diǎn)區(qū)別,也就帶來了二叉樹求和與解釋器算法的區(qū)別。對(duì)二叉樹進(jìn)行求和的時(shí)候,在每個(gè)子樹節(jié)點(diǎn),我們都做加法。而對(duì)表達(dá)式進(jìn)行解釋的時(shí)候,在每一個(gè)子樹節(jié)點(diǎn),我們不一定進(jìn)行加法。根據(jù)子樹的“操作符”不同,我們可能會(huì)選擇加,減,乘,除四種操作。
好了,下面就是這個(gè)計(jì)算器的代碼。它接受一個(gè)表達(dá)式,輸出一個(gè)數(shù)字作為結(jié)果。
#lang racket ; 聲明用 Racket 語言(define calc(lambda (exp)(match exp ; 分支匹配:表達(dá)式的兩種情況[(? number? x) x] ; 是數(shù)字,直接返回[`(,op ,e1 ,e2) ; 匹配提取操作符op和兩個(gè)操作數(shù)e1,e2(let ([v1 (calc e1)] ; 遞歸調(diào)用 calc 自己,得到 e1 的值 [v2 (calc e2)]) ; 遞歸調(diào)用 calc 自己,得到 e2 的值(match op ; 分支匹配:操作符 op 的 4 種情況 ['+ (+ v1 v2)] ; 如果是加號(hào),輸出結(jié)果為 (+ v1 v2) ['- (- v1 v2)] ; 如果是減號(hào),乘號(hào),除號(hào),相似的處理 ['* (* v1 v2)]['/ (/ v1 v2)]))])))你可以得到如下的結(jié)果:
(calc '(+ 1 2)) ;; => 3 (calc '(* 2 3)) ;; => 6 (calc '(* (+ 1 2) (+ 3 4))) ;; => 21跟之前的二叉樹求和代碼比較一下,你會(huì)發(fā)現(xiàn)它們驚人的相似,因?yàn)榻忉屍鞅緛砭褪且粋€(gè)樹遍歷算法。不過你發(fā)現(xiàn)它們有什么不同嗎?它們的不同點(diǎn)在于:
算術(shù)表達(dá)式的模式里面,多出了一個(gè)“操作符”(op)葉節(jié)點(diǎn):(,op ,e1 ,e2)
對(duì)子樹 e1 和 e2 分別求值之后,我們不是返回 (+ v1 v2),而是根據(jù) op 的不同,返回不同的結(jié)果:
(match op['+ (+ v1 v2)]['- (- v1 v2)]['* (* v1 v2)]['/ (/ v1 v2)])最后你發(fā)現(xiàn),一個(gè)算術(shù)表達(dá)式的解釋器,不過是一個(gè)稍加擴(kuò)展的樹遍歷算法。
R2:一個(gè)很小的程序語言
實(shí)現(xiàn)了一個(gè)計(jì)算器,現(xiàn)在讓我們過渡到一種更強(qiáng)大的語言。為了方便稱呼,我給它起了一個(gè)萌萌噠名字,叫 R2。R2 比起之前的計(jì)算器,只多出四個(gè)元素,它們分別是:變量,函數(shù),綁定,調(diào)用。再加上之前介紹的算術(shù)操作,我們就得到一個(gè)很簡單的程序語言,它只有5種不同的構(gòu)造。用 Scheme 的語法,這5種構(gòu)造看起來就像這樣:
變量:x
函數(shù):(lambda (x) e)
綁定:(let ([x e1]) e2)
調(diào)用:(e1 e2)
算術(shù):(? e2 e2)
(其中,? 是一個(gè)算術(shù)操作符,可以選擇 +, -, *, / 其中之一)
一般程序語言還有很多其它構(gòu)造,可是一開頭就試圖去實(shí)現(xiàn)所有那些,只會(huì)讓人糊涂。最好是把這少數(shù)幾個(gè)東西搞清楚,確保它們正確之后,才慢慢加入其它元素。
這些構(gòu)造的語義,跟 Scheme 里面的同名構(gòu)造幾乎一模一樣。如果你不清楚什么是”綁定“,那你可以把它看成是普通語言里的”變量聲明“。
需要注意的是,跟一般語言不同,我們的函數(shù)只接受一個(gè)參數(shù)。這不是一個(gè)嚴(yán)重的限制,因?yàn)樵谖覀兊恼Z言里,函數(shù)可以被作為值傳遞,也就是所謂“first-class function”。所以你可以用嵌套的函數(shù)定義來表示有兩個(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) (+ x y))) 1) 2) ;; => 3這種做法在PL術(shù)語里面,叫做咖喱(currying)。看起來啰嗦,但這樣我們的解釋器可以很簡單。等我們理解了基本的解釋器,再實(shí)現(xiàn)真正的多參數(shù)函數(shù)也不遲。
另外,我們的綁定語法 (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 ([x 1])(let ([y 2])(+ x y)))R2 的解釋器
下面是我們今天要完成的解釋器,它可以運(yùn)行一個(gè) R2 程序。你可以先留意一下各部分的注釋。
#lang racket;;; 以下三個(gè)定義 env0, ext-env, lookup 是對(duì)環(huán)境(environment)的基本操作:;; 空環(huán)境 (define env0 '());; 擴(kuò)展。對(duì)環(huán)境 env 進(jìn)行擴(kuò)展,把 x 映射到 v,得到一個(gè)新的環(huán)境 (define ext-env(lambda (x v env)(cons `(,x . ,v) env)));; 查找。在環(huán)境中 env 中查找 x 的值。如果沒找到就返回 #f (define lookup(lambda (x env)(let ([p (assq x env)])(cond[(not p) #f][else (cdr p)]))));; 閉包的數(shù)據(jù)結(jié)構(gòu)定義,包含一個(gè)函數(shù)定義 f 和它定義時(shí)所在的環(huán)境 (struct Closure (f env));; 解釋器的遞歸定義(接受兩個(gè)參數(shù),表達(dá)式 exp 和環(huán)境 env) ;; 共 5 種情況(變量,函數(shù),綁定,調(diào)用,數(shù)字,算術(shù)表達(dá)式) (define interp(lambda (exp env)(match exp ; 對(duì)exp進(jìn)行模式匹配[(? symbol? x) ; 變量(let ([v (lookup x env)])(cond [(not v) (error "undefined variable" x)] [else v]))] [(? number? x) x] ; 數(shù)字[`(lambda (,x) ,e) ; 函數(shù)(Closure exp env)][`(let ([,x ,e1]) ,e2) ; 綁定(let ([v1 (interp e1 env)])(interp e2 (ext-env x v1 env)))][`(,e1 ,e2) ; 調(diào)用(let ([v1 (interp e1 env)] [v2 (interp e2 env)])(match v1 [(Closure `(lambda (,x) ,e) env-save) (interp e (ext-env x v2 env-save))]))][`(,op ,e1 ,e2) ; 算術(shù)表達(dá)式(let ([v1 (interp e1 env)] [v2 (interp e2 env)])(match op ['+ (+ v1 v2)] ['- (- v1 v2)] ['* (* v1 v2)]['/ (/ v1 v2)]))])));; 解釋器的“用戶界面”函數(shù)。它把 interp 包裝起來,掩蓋第二個(gè)參數(shù),初始值為 env0 (define r2(lambda (exp)(interp exp env0)))這里有一些測(cè)試?yán)?#xff1a;
(r2 '(+ 1 2)) ;; => 3(r2 '(* 2 3)) ;; => 6(r2 '(* 2 (+ 3 4))) ;; => 14(r2 '(* (+ 1 2) (+ 3 4))) ;; => 21(r2 '((lambda (x) (* 2 x)) 3)) ;; => 6(r2 '(let ([x 2])(let ([f (lambda (y) (* x y))])(f 3)))) ;; => 6(r2 '(let ([x 2])(let ([f (lambda (y) (* x y))])(let ([x 4])(f 3))))) ;; => 6在接下來的幾節(jié),我們來仔細(xì)看看這個(gè)解釋器的各個(gè)部分。
對(duì)基本算術(shù)操作的解釋
算術(shù)操作一般都是程序里最基本的構(gòu)造,它們不能再被細(xì)分為多個(gè)步驟,所以我們先來看看對(duì)算術(shù)操作的處理。以下就是 R2 解釋器處理算術(shù)的部分,它是 interp 的最后一個(gè)分支。
(match exp... ...[`(,op ,e1 ,e2)(let ([v1 (interp e1 env)] ; 遞歸調(diào)用 interp 自己,得到 e1 的值[v2 (interp e2 env)]) ; 遞歸調(diào)用 interp 自己,得到 e2 的值(match op ; 分支:處理操作符 op 的 4 種情況['+ (+ v1 v2)] ; 如果是加號(hào),輸出結(jié)果為 (+ v1 v2)['- (- v1 v2)] ; 如果是減號(hào),乘號(hào),除號(hào),相似的處理['* (* v1 v2)]['/ (/ v1 v2)]))])你可以看到它幾乎跟剛才寫的計(jì)算器一模一樣,不過現(xiàn)在 interp 的調(diào)用多了一個(gè)參數(shù) env 而已。這個(gè) env 是所謂“環(huán)境”,我們下面很快就講。
對(duì)數(shù)字的解釋
對(duì)數(shù)字的解釋很簡單,把它們?cè)獠粍?dòng)返回就可以了。
[(? number? x) x]變量和函數(shù)
變量和函數(shù)是解釋器里最麻煩的部分,所以我們來仔細(xì)看看。
變量(variable)的產(chǎn)生,是數(shù)學(xué)史上的最大突破之一。因?yàn)樽兞靠梢员唤壎ǖ讲煌闹?#xff0c;從而使函數(shù)的實(shí)現(xiàn)成為可能。比如數(shù)學(xué)函數(shù) f(x) = x * 2,其中 x 是一個(gè)變量,它把輸入的值傳遞到函數(shù)體 x * 2 里面。如果沒有變量,函數(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)插座,電流就通過這根電線到達(dá)另外一端。如果電線導(dǎo)電性能良好,兩頭的電壓應(yīng)該相等。
環(huán)境
我們的解釋器只能一步一步的做事情。比如,當(dāng)它需要求 f(1) 的值的時(shí)候,它分成兩步操作:
1.把 x 綁定到 1,這樣函數(shù)體內(nèi)才能看見這個(gè)綁定。
2.進(jìn)入 f 的函數(shù)體,對(duì) x * 2 進(jìn)行求值。
這就像一個(gè)人做出這兩個(gè)動(dòng)作:
1.把插頭插進(jìn)插座 。
2.到電線的另外一頭,測(cè)量它的電壓,并且把結(jié)果乘以 2。
在第一步和第二步之間,我們?nèi)绾斡涀?x 的值呢?通過所謂“環(huán)境”!我們用環(huán)境記錄變量的值,并且把它們傳遞到變量的“可見區(qū)域”。變量的可見區(qū)域,用術(shù)語說叫做“作用域”(scope)。
在我們的解釋器里,用于處理環(huán)境的代碼如下:
;; 空環(huán)境 (define env0 '());; 對(duì)環(huán)境 env 進(jìn)行擴(kuò)展,把 x 映射到 v (define ext-env(lambda (x v env)(cons `(,x . ,v) env)));; 取值。在環(huán)境中 env 中查找 x 的值 (define lookup(lambda (x env)(let ([p (assq x env)])(cond[(not p) #f][else (cdr p)]))))這里我們用一種最簡單的數(shù)據(jù)結(jié)構(gòu),Scheme 的 association list,來表示環(huán)境。Association list 看起來像這個(gè)樣子:((x . 1) (y . 2) (z . 5))。它是一個(gè)兩元組(pair)的鏈表,左邊的元素是 key,右邊的元素是 value。寫得直觀一點(diǎn)就是:
((x . 1)(y . 2)(z . 5))查表操作就是從頭到尾搜索,如果左邊的 key 是要找的變量,就返回整個(gè) pair。簡單吧?效率很低,但是足夠完成我們現(xiàn)在的任務(wù)。
ext-env 函數(shù)擴(kuò)展一個(gè)環(huán)境。比如,如果原來的環(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 ([x 1]) ; env='()。綁定x到1。(let ([y 2]) ; env='((x . 1))。綁定y到2。(let ([x 3]) ; env='((y . 2) (x . 1))。綁定x到3。(+ x y)))) ; 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),所以它被忽略了。
這并不等于說 (x . 1) 就可以被改寫或者丟棄,因?yàn)樗匀皇怯杏玫摹D阒恍枰匆粋€(gè)稍微不同的例子,就知道這是怎么回事:
(let ([x 1]) ; env='()。綁定x到1。(+ (let ([x 2]) ; 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。這很符合直覺,因?yàn)?x 總是找到最內(nèi)層的定義。
值得注意的是,環(huán)境被擴(kuò)展以后,形成了一個(gè)新的環(huán)境,而原來的環(huán)境并沒有被改變。比如,上面的 ((y . 2) (x . 1)) 并沒有刪除或者修改,只不過是被“引用”到一個(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)入遞歸,返回之后,外層的代碼必須仍然可以訪問原來外層的環(huán)境。
當(dāng)然,我們也可以用另外的,更高效的數(shù)據(jù)結(jié)構(gòu)(比如平衡樹,串接起來的哈希表)來表示環(huán)境。如果你學(xué)究一點(diǎn),甚至可以用函數(shù)來表示環(huán)境。這里為了代碼簡單,我們選擇了最笨,然而正確,容易理解的數(shù)據(jù)結(jié)構(gòu)。
對(duì)變量的解釋
了解了變量,函數(shù)和環(huán)境,我們來看看解釋器對(duì)變量的“取值”操作,也就是 match 的第一種情況。
[(? symbol? x) (lookup x env)]
這就是在環(huán)境中,沿著從內(nèi)向外的“作用域順序”,查找變量的值。
這里的 (? symbol? x) 是一種特殊的模式,它使用 Scheme 函數(shù) symbol? 來判斷輸入是否是一個(gè)符號(hào),如果是,就把它綁定到 x,然后你就可以在右邊用 x 來指稱這個(gè)輸入。
對(duì)綁定的解釋
現(xiàn)在我們來看看對(duì) let 綁定的解釋:
[`(let ([,x ,e1]) ,e2) (let ([v1 (interp e1 env)]) ; 解釋右邊表達(dá)式e1,得到值v1(interp e2 (ext-env x v1 env)))] ; 把(x . v1)擴(kuò)充到環(huán)境頂部,對(duì)e2求值通過代碼里的注釋,你也許已經(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è)微妙的問題,很容易弄錯(cuò),這是由于函數(shù)體內(nèi)也許會(huì)含有外層的變量,叫做“自由變量”。所以在分析函數(shù)的代碼之前,我們來了解一下不同的“作用域”(scoping)規(guī)則。
我們舉個(gè)例子來解釋這個(gè)問題。下面這段代碼,它的值應(yīng)該是多少呢?
(let ([x 2])(let ([f (lambda (y) (* x y))])(let ([x 4])(f 3))))在這里,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è)綁定呢?這似乎無關(guān)痛癢,然而當(dāng)我們調(diào)用 (f 3) 的時(shí)候,嚴(yán)重的問題來了。f 的函數(shù)體是 (* x y),我們知道 y 的值來自參數(shù) 3,可是 x 的值是多少呢?它應(yīng)該是2,還是4呢?
在歷史上,這段代碼可能有兩種不同的結(jié)果,這種區(qū)別一直延續(xù)到今天。如果你在 Scheme (Racket)里面寫以上的代碼,它的結(jié)果是6。
;; Scheme (let ([x 2])(let ([f (lambda (y) (* x y))])(let ([x 4])(f 3))));; => 6現(xiàn)在我們來看看,在 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。
那么哪一種方式更好呢?或者用哪一種都無所謂?答案是,dynamic scoping 是非常錯(cuò)誤的做法。歷史的教訓(xùn)告訴我們,它會(huì)帶來許許多多莫名其妙的 bug,導(dǎo)致 dynamic scoping 的語言幾乎完全沒法用。這是為什么呢?
原因在于,像 (let ((x 4)) …) 這樣的變量綁定,只應(yīng)該影響它內(nèi)部“看得見”的 x 的值。當(dāng)我們看見 (let ((x 4)) (f 3)) 的時(shí)候,并沒有在 let 的內(nèi)部看見任何叫“x” 的變量,所以我們“直覺”的認(rèn)為,(let ((x 4)) …) 對(duì) x 的綁定,不應(yīng)該引起 (f 3) 的結(jié)果變化。
然而對(duì)于 dynamic scoping,我們的直覺卻是錯(cuò)誤的。因?yàn)?f 的函數(shù)體里面有一個(gè) x,雖然我們沒有在 (f 3) 這個(gè)調(diào)用里面看見它,然而它卻存在于 f 定義的地方。要知道,f 定義的地方也許隔著幾百行代碼,甚至在另外一個(gè)文件里面。而且調(diào)用函數(shù)的人憑什么應(yīng)該知道, f 的定義里面有一個(gè)自由變量,它的名字叫做 x?所以 dynamic scoping 在設(shè)計(jì)學(xué)的角度來看,是一個(gè)反人類的設(shè)計(jì) :)
相反,lexical scoping 卻是符合人們直覺的。雖然在 (let ((x 4)) (f 3)) 里面,我們把 x 綁定到了 4,然而 f 的函數(shù)體并不是在那里定義的,我們也沒在那里看見任何 x,所以 f 的函數(shù)體里面的 x,仍然指向我們定義它的時(shí)候看得見的那個(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):
(struct Closure (f env))有了這個(gè)數(shù)據(jù)結(jié)構(gòu),我們對(duì) (lambda (x) e) 的解釋就可以寫成這樣:
[`(lambda (,x) ,e)(Closure exp env)]注意這里的 exp 就是 `(lambda (,x) ,e) 自己。
有意思的是,我們的解釋器遇到 (lambda (x) e),幾乎沒有做任何計(jì)算。它只是把這個(gè)函數(shù)包裝了一下,把它與當(dāng)前的環(huán)境一起,打包放到一個(gè)數(shù)據(jù)結(jié)構(gòu)(Closure)里面。這個(gè)閉包結(jié)構(gòu),記錄了我們?cè)诤瘮?shù)定義的位置“看得見”的那個(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 (interp e1 env)] ; 計(jì)算函數(shù) e1 的值[v2 (interp e2 env)]) ; 計(jì)算參數(shù) e2 的值(match v1[(Closure `(lambda (,x) ,e) env-save) ; 用模式匹配的方式取出閉包里的各個(gè)子結(jié)構(gòu)(interp e (ext-env x v2 env-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)插座,使它開始運(yùn)轉(zhuǎn)。比如,當(dāng) (lambda (x) (* x 2)) 被作用于 1 時(shí),我們把 x 綁定到 1,然后解釋它的函數(shù)體 (* x 2)。但是這里有一個(gè)問題,函數(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 取出來,那我們就可以查詢它,得到函數(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 難道這里就不用了嗎?是的。我們通過 env 來計(jì)算 e1 和 e2 的值,是因?yàn)?e1 和 e2 里面的變量,在“當(dāng)前環(huán)境”(env)里面看得見。可是函數(shù)體的定義,在當(dāng)前環(huán)境下是看不見的。它的代碼在別的地方,而那個(gè)地方看得見的環(huán)境,被我們存在閉包里了,它就是 env-save。所以我們把 v1 里面的閉包環(huán)境 env-save 取出來,用于計(jì)算函數(shù)體的值。
有意思的是,如果我們用 env,而不是env-save 來解釋函數(shù)體,那我們的語言就變成了 dynamic scoping。現(xiàn)在來實(shí)驗(yàn)一下:你可以把 (interp e (ext-env x v2 env-save)) 里面的 env-save 改成 env,再試試我們之前討論過的代碼,它的輸出就會(huì)變成 12。那就是我們之前講過的,dynamic scoping 的結(jié)果。
(r2 '(let ([x 2])(let ([f (lambda (y) (* x y))])(let ([x 4])(f 3)))));; => 12你也許發(fā)現(xiàn)了,如果我們的語言是 dynamic scoping,那就沒必要使用閉包了,因?yàn)槲覀兏静恍枰]包里面保存的環(huán)境。這樣一來,dynamic scoping 的解釋器就可以寫成這樣:
(define interp(lambda (exp env)(match exp ... ...[`(lambda (,x) ,e) ; 函數(shù):直接返回自己的表達(dá)式exp]... ...[`(,e1 ,e2) (let ([v1 (interp e1 env)] [v2 (interp e2 env)])(match v1 [`(lambda (,x) ,e) ; 調(diào)用:直接使用函數(shù)的表達(dá)式本身 (interp e (ext-env x v2 env))]))]... ... )))注意到這個(gè)解釋器的函數(shù)有多容易實(shí)現(xiàn)嗎?它就是這個(gè)函數(shù)的表達(dá)式自己,原封不動(dòng)。用函數(shù)的表達(dá)式本身來表示它的值,是很直接很簡單的做法,也是大部分人一開頭就會(huì)想到的。然而這樣實(shí)現(xiàn)出來的語言,就不知不覺地采用了 dynamic scoping。
這就是為什么很多早期的 Lisp 語言,比如 Emacs Lisp,都使用 dynamic scoping。這并不是因?yàn)樗鼈兊脑O(shè)計(jì)者在 dynamic scoping 和 lexical scoping 兩者之中做出了選擇,而是因?yàn)槭褂煤瘮?shù)的表達(dá)式本身來作為它的值,是最直接,一般人都會(huì)首先想到的做法。
另外,在這里我們也看到環(huán)境用“函數(shù)式數(shù)據(jù)結(jié)構(gòu)”表示的好處。閉包被調(diào)用時(shí)它的環(huán)境被擴(kuò)展,但是這并不會(huì)影響原來的那個(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è)瓉憝h(huán)境的內(nèi)容。所以你在函數(shù)返回之后必須刪除參數(shù)的綁定。這樣不但麻煩,而且在復(fù)雜的情況下很容易出錯(cuò)。
思考題:可能有些人看過 lambda calculus,這些人可能知道 (let ([x e1]) e2) 其實(shí)等價(jià)于一個(gè)函數(shù)調(diào)用:((lambda (x) e2) e1)。現(xiàn)在問題來了,我們?cè)谟懻摵瘮?shù)和調(diào)用的時(shí)候,很深入的討論了關(guān)于 lexical scoping 和 dynamic scoping 的差別。既然 let 綁定等價(jià)于一個(gè)函數(shù)定義和調(diào)用,為什么之前我們討論對(duì)綁定的時(shí)候,沒有討論過 lexical scoping 和 dynamic scoping 的問題,也沒有制造過閉包呢?
不足之處
現(xiàn)在你已經(jīng)學(xué)會(huì)了如何寫出一個(gè)簡單的解釋器,它可以處理一個(gè)相當(dāng)強(qiáng)的,具有“first-class 函數(shù)”的語言。出于教學(xué)的考慮,這個(gè)解釋器并沒有考慮實(shí)用的需求,所以它并不能作為“工業(yè)應(yīng)用”。在這里,我指出它的一些不足之處。
1.缺少必要的語言構(gòu)造。我們的語言里缺少好些實(shí)用語言必須的構(gòu)造:遞歸,數(shù)組,賦值操作,字符串,自定義數(shù)據(jù)結(jié)構(gòu),…… 作為一篇基礎(chǔ)性的讀物,我不能把這些都加進(jìn)來。如果你對(duì)這些有興趣,可以看看其它書籍,或者等待我的后續(xù)作品。
2.不合法代碼的檢測(cè)和報(bào)告。你也許發(fā)現(xiàn)了,這個(gè)解釋器的 match 表達(dá)式,全都假定了輸入都是合法的程序,它并沒有檢查不合法的情況。如果你給它一個(gè)不合法的程序,它的行為會(huì)變得詭異。一個(gè)實(shí)用的解釋器,必須加入對(duì)代碼格式進(jìn)行全面檢測(cè),報(bào)告不合法的代碼結(jié)構(gòu)。
3.低效率的數(shù)據(jù)結(jié)構(gòu)。在 association list 里面查找變量,是線性的復(fù)雜度。當(dāng)程序有很多變量的時(shí)候就有性能問題。一個(gè)實(shí)用的解釋器,需要更高效的數(shù)據(jù)結(jié)構(gòu)。這種數(shù)據(jù)結(jié)構(gòu)不一定非得是函數(shù)式的。你也可以用非函數(shù)式的數(shù)據(jù)結(jié)構(gòu)(比如哈希表),經(jīng)過一定的改造,達(dá)到同樣的性質(zhì),卻具有更高的效率。 ? 另外,你還可以把環(huán)境轉(zhuǎn)化成一個(gè)數(shù)組。給環(huán)境里的每個(gè)變量分配一個(gè)下標(biāo)(index),在這個(gè)數(shù)組里就可以找到它的值。如果你用數(shù)組表示環(huán)境,那么這個(gè)解釋器就向編譯器邁進(jìn)了一步。
4.S表達(dá)式的歧義問題。為了教學(xué)需要,我們的解釋器直接使用S表達(dá)式來表達(dá)語法樹,用模式匹配來進(jìn)行分支遍歷。在實(shí)際的語言里,這種方式會(huì)帶來比較大的問題。因?yàn)镾表達(dá)式是一種通用的數(shù)據(jù)結(jié)構(gòu),用它表示的東西,看起來都差不多的樣子。一旦程序的語法構(gòu)造多起來,直接對(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á)式上寫解釋器,而是先寫一個(gè)“parser”,這個(gè)parser把S表達(dá)式轉(zhuǎn)換成 Racket 的 struct 結(jié)構(gòu)。然后解釋器再在 struct 上面進(jìn)行分支匹配。這樣解釋器不用擔(dān)心歧義問題,而且會(huì)帶來效率的提升。
總結(jié)
- 上一篇: rpgmakermv导出html,Rpg
- 下一篇: 《挑战程序设计竞赛》2.2 贪心法-其它