第十四章 宏和編譯(Macros and Compilation)
14.1 導(dǎo)語(yǔ)
宏函數(shù),縮略玩荠,宏肩狂。是擴(kuò)展lisp語(yǔ)法的一種方式。在本章我們會(huì)使用求值回溯圖和一些叫做ppmx的小工具(定義在lisp toolkit章節(jié))來(lái)看看宏是如何工作的沼头。會(huì)有一些材料需要參考前面的進(jìn)階話題章節(jié)。如果你還沒(méi)有看過(guò)的話歹篓,我會(huì)標(biāo)明去哪里看的瘫证。
在本章的后半部分我們會(huì)學(xué)習(xí)一下編譯(compilation)揉阎。如果你覺(jué)得你的程序中有一部分運(yùn)行的太慢,你可以將他們編譯一下來(lái)讓他快一些背捌。編譯器將lisp程序翻譯成機(jī)器語(yǔ)言程序毙籽,這樣他的執(zhí)行速度會(huì)有10倍到100倍的提升。
14.2 宏就是縮寫(xiě)法
在計(jì)算機(jī)中的宏實(shí)際上就是縮寫(xiě)法毡庆,任何你想要寫(xiě)成所寫(xiě)的東西都可以用更加詳細(xì)的語(yǔ)言來(lái)表示坑赡,只是會(huì)更長(zhǎng)一些。相似的Common Lisp宏不會(huì)讓你寫(xiě)出任何用一般函數(shù)不能表示的功能么抗。但是他確實(shí)會(huì)幫你把功能寫(xiě)的更加精煉毅否。incf函數(shù)就是一個(gè)好例子,(incf a)肯定比(setf a (+ A 1))寫(xiě)起來(lái)要精煉的多吧蝇刀。
有一些宏是非常精妙的螟加,特別是像setf和incf這種一般賦值宏,他可以將任意的復(fù)雜位置描述解釋成一般的變量引用吞琐。
當(dāng)上面表達(dá)式里的位置描述被解析出來(lái)的時(shí)候捆探,你會(huì)驚嘆incf的強(qiáng)大能力。
宏可以用簡(jiǎn)單的指令生成復(fù)雜的程序站粟。比如destruct宏黍图,可以講starship的結(jié)構(gòu)定義轉(zhuǎn)化成一個(gè)指令流來(lái)支持starship的數(shù)據(jù)類型。這個(gè)指令流包括了make-starshiphe starship-p的函數(shù)定義奴烙,還有所有組件的訪問(wèn)函數(shù)助被。當(dāng)然所有的工作并不只是包括函數(shù)定義,defstruct的輸出取決于具體實(shí)現(xiàn)的定義切诀。例如揩环,starship整合進(jìn)Common Lisp類型繼承機(jī)構(gòu)的方式就是每一個(gè)實(shí)現(xiàn)都是不同的。defstruct包含的函數(shù)和變量都不是Common Lisp標(biāo)準(zhǔn)的一部分趾牧,甚至不會(huì)被lisp供應(yīng)商寫(xiě)進(jìn)文檔里检盼。defstruct宏允許lisp供應(yīng)商提供一個(gè)協(xié)議來(lái)對(duì)消費(fèi)者隱藏具體細(xì)節(jié),定義結(jié)構(gòu)體的標(biāo)準(zhǔn)方式取決于具體的Common Lisp實(shí)現(xiàn)翘单。
14.3 宏展開(kāi)
如果你用縮略法寫(xiě)了一些東西的話,為了便于理解和使用蹦渣,你終歸是要把他展開(kāi)的哄芜。lisp會(huì)把宏調(diào)用自動(dòng)展開(kāi),一個(gè)宏實(shí)際上就是一種特殊的縮略法柬唯,展開(kāi)宏函數(shù)的時(shí)候并不會(huì)對(duì)它的參數(shù)求值认臊。展開(kāi)的工作時(shí)尋找它的參數(shù)還有生成一個(gè)lisp可以求值的表達(dá)式。在表達(dá)式(incf a)中锄奢,宏incf被調(diào)用失晴,參數(shù)就是A(不求值)剧腻。他會(huì)構(gòu)建一個(gè)表達(dá)式,(setq a (+ A 1))涂屁,之后返回书在。incf準(zhǔn)確構(gòu)建的表達(dá)式取決于具體實(shí)現(xiàn),但是看上去應(yīng)該是setq拆又。lisp之后會(huì)對(duì)表達(dá)式求值儒旬,然后對(duì)A的值加上1。
回憶一下我們?cè)?0.10章節(jié)講的特殊函數(shù)setq帖族,當(dāng)setf給一個(gè)普通變量賦值的時(shí)候栈源,實(shí)際上是在內(nèi)部調(diào)用setq函數(shù)。這個(gè)setf宏函數(shù)實(shí)際上是展開(kāi)成了一個(gè)setq函數(shù)的調(diào)用竖般。
在求值回溯圖中甚垦,宏展開(kāi)使用一條虛線來(lái)表示,宏返回的表達(dá)式會(huì)被正常求值涣雕。
如果你想要看一下計(jì)算機(jī)內(nèi)部的宏展開(kāi)艰亮,你可以使用一個(gè)叫做ppmx的小工具,定義在本章節(jié)的lisptoolkit單元胞谭。ppmx這個(gè)名字是pretty print macro expansion(吐槽:漂亮滴打印展開(kāi)啊喂@取)的縮寫(xiě),一些lisp實(shí)現(xiàn)也會(huì)有打印展開(kāi)的工具丈屹。
在一些lisp實(shí)現(xiàn)中调俘,incf的宏展開(kāi)是不一樣的,例如旺垒,incf可能會(huì)展開(kāi)成一個(gè)let表達(dá)式來(lái)創(chuàng)造一個(gè)本地變量保存(+ A 1)的值彩库,然后再重新傳遞給A∠冉看上去這好像并不是一個(gè)直接的方法來(lái)給a加1.但是請(qǐng)記住骇钦,incf的目的是更加復(fù)雜的結(jié)構(gòu)也能進(jìn)行增值操作,在哪些情況下竞漾,let或許是一個(gè)更必要的選擇眯搭。
在上面的例子中,#:G0144是一個(gè)內(nèi)部符號(hào)业岁,叫做內(nèi)生變量(gensym(查了一下鳞仙,不是英語(yǔ)單詞,是造的詞笔时,專家系統(tǒng)的意思))棍好,是由incf函數(shù)自動(dòng)生成的變量,作為一個(gè)本地變量名使用。內(nèi)生變量確保不會(huì)和你已有的任何變量名產(chǎn)生沖突借笙。出于我們不需要了解的理由扒怖,#:G0144和符號(hào)G0144是不同的,你不能從鍵盤(pán)輸入這樣的符號(hào)业稼,所以他也不可能和你的變量名沖突盗痒,甚至你特意選擇G0144作為名字。
14.4 定義一個(gè)宏
宏是由defmarco來(lái)定義的盼忌,它的語(yǔ)法和defun類似积糯。我們來(lái)定義一個(gè)給普通變量加上1的incf的簡(jiǎn)單版本。我們紅會(huì)接受一個(gè)變量名作為輸入谦纱,冰鞋構(gòu)造一個(gè)增量表達(dá)式看成。
另一個(gè)看simple-incf如何工作的方法就是用dtrace來(lái)追蹤他的行為。(如果你使用的標(biāo)準(zhǔn)的trace而不是dtrace的話罵你可能不能追蹤宏跨嘉。)
追蹤你自己寫(xiě)的宏是沒(méi)有問(wèn)題的川慌,但是在一些lisp實(shí)現(xiàn)當(dāng)中最好不要追蹤那寫(xiě)重要的內(nèi)建函數(shù),比如setf祠乃。如果追蹤這些函數(shù)出現(xiàn)了問(wèn)題梦重,換成ppmx試下。使用ppmx的好處之一就是宏展開(kāi)的結(jié)果只是被打印亮瓷,但是不會(huì)被求值琴拧。ppmx允許你使用任意表達(dá)式來(lái)嘗試宏展開(kāi)而不用擔(dān)心會(huì)發(fā)生錯(cuò)誤。
現(xiàn)在我們來(lái)修改一下simple-incf嘱支,可以接受第二個(gè)參數(shù)蚓胸,定義增值的幅度。我們使用lambda列表關(guān)鍵字&optional來(lái)實(shí)現(xiàn)除师。(可選參數(shù)在章節(jié)11.13里有解釋過(guò))對(duì)變量默認(rèn)的增值數(shù)字是1沛膳。
宏不會(huì)對(duì)它的參數(shù)進(jìn)行求值,所以simple-incf的輸入是字符b和列表(* 3 A)汛聚,不是數(shù)字2和15.求值回溯圖展示了simple-incf如何計(jì)算出宏展開(kāi)锹安,之后lisp才求值。
現(xiàn)在我們來(lái)思考一下倚舀,為什么incf一定要是一個(gè)宏而不是一個(gè)函數(shù)呢叹哭?假設(shè)我們頂一個(gè)函數(shù)incf,用defun定義痕貌,就叫他faulty-incf好了:
既然faulty-incf是一個(gè)函數(shù)话速,那么他就會(huì)對(duì)它的參數(shù)求值,并且也沒(méi)有返回一個(gè)表達(dá)式給lisp求值的打算芯侥,僅僅是自己把事情給辦了。但是它的參數(shù)已經(jīng)被求值了之后,就會(huì)出現(xiàn)一個(gè)問(wèn)題柱查。
faulty-incf的輸入是7廓俭,他創(chuàng)造了一個(gè)叫var的本地變量,然后將輸入存儲(chǔ)進(jìn)去唉工,之后再將var的值加上1.他對(duì)變量A根本就沒(méi)有概念研乒,因?yàn)樵诤瘮?shù)被輸入之前,參數(shù)就應(yīng)被求值了淋硝。
我們可能嘗試在變量a被傳給faulty-incf的時(shí)候給他加上一個(gè)引號(hào)雹熬,當(dāng)然必須是要更改faulty-incf的定義,因?yàn)檩斎氩辉偈且粋€(gè)數(shù)字谣膳。但是因?yàn)槟承├碛桑〞?huì)在進(jìn)階話題里解釋)竿报,這也是不起作用的。simple-incf必須被寫(xiě)成一個(gè)宏继谚,這并不和我們先前說(shuō)的宏是縮略法的說(shuō)法沖突烈菌。我們?nèi)稳豢梢宰杂墒褂胹etq表達(dá)式來(lái)代替simple-incf。setq是一個(gè)特殊函數(shù)花履,不是一個(gè)宏芽世,他們之間的區(qū)別會(huì)在下一節(jié)解釋。
14.5 宏是語(yǔ)法擴(kuò)展
既然宏的目的是擴(kuò)展語(yǔ)言本身的語(yǔ)法诡壁,lisp就不會(huì)像對(duì)待普通調(diào)用那樣對(duì)待宏調(diào)用济瓢。在普通調(diào)用和宏調(diào)用之間有三個(gè)重要區(qū)別。
- 普通函數(shù)的參數(shù)請(qǐng)示被求值的妹卿,宏函數(shù)的參數(shù)不被求值旺矾。
- 普通函數(shù)的結(jié)果可以使任何東西,宏函數(shù)返回的結(jié)果肯定是一個(gè)合法的lisp表達(dá)式纽帖。
- 宏函數(shù)返回一個(gè)表達(dá)式之后宠漩,表達(dá)式是馬上被求值的,普通函數(shù)返回的結(jié)果是不被求值的懊直。
除了宏之外扒吁,common lisp還包括了一部分特殊函數(shù),比如說(shuō)setq室囊,if雕崩,let,和block融撞。特殊函數(shù)是構(gòu)建Common Lisp的最底層模塊盼铁,他們主要負(fù)責(zé)像賦值,作用域尝偎,基本控制結(jié)構(gòu)饶火,塊和循環(huán)鹏控。類似于宏,特殊函數(shù)并不對(duì)他們的參數(shù)求值肤寝,但是他們也不返回被求值的表達(dá)式当辐。他們是做很特殊的事情的原始函數(shù)。你不能寫(xiě)一個(gè)新的特殊函數(shù)鲤看,只有Lisp實(shí)現(xiàn)者才可以缘揪。
回到我們關(guān)于宏就是縮略法的討論。我們會(huì)說(shuō)任何宏可以做到的事情义桂,沒(méi)有了宏只用普通函數(shù)和特殊函數(shù)等等的組合也可以做的到找筝。
14.6 反引號(hào)字符
函數(shù)simple-incf通過(guò)兩個(gè)list調(diào)用,還有一些引號(hào)字符慷吊,加上變量var和amoun的值構(gòu)造了一個(gè)lisp表達(dá)式袖裕。在表達(dá)式規(guī)模比較小的時(shí)候,這個(gè)方法是可以的罢浇,但是當(dāng)宏函數(shù)必須生成龐大復(fù)雜的表達(dá)式的時(shí)候陆赋,一個(gè)個(gè)構(gòu)造就顯得很麻煩了。我們所需要的是一個(gè)替代的方法來(lái)實(shí)現(xiàn)寫(xiě)一個(gè)表達(dá)式宏的模板嚷闭。止嘔所有的宏都必須由空格填滿攒岛,反引號(hào)就是起那個(gè)作用。
反引號(hào)字符(`)和引號(hào)是相似的胞锰,他們都被用在引號(hào)列表中灾锯。然而,在一個(gè)反引號(hào)列表中,前導(dǎo)逗號(hào)的存在作為表達(dá)式?jīng)]有引用的標(biāo)記嗅榕,意思就是表達(dá)式的值而不是表達(dá)式會(huì)被使用顺饮。
我們可以使用反引號(hào)表達(dá)式來(lái)寫(xiě)一個(gè)simpleincf的更加精細(xì)的版本。
宏的一個(gè)很普遍的用法就是去避免引用參數(shù)凌那。宏使用需要的參數(shù)的引用版本來(lái)展開(kāi)成一個(gè)普通函數(shù)調(diào)用兼雄。你可以使用反引號(hào)去生成表達(dá)式,并且在其間夾雜引號(hào)帽蝶,如下面的模板赦肋。
在下面的例子中,two-from-one宏函數(shù)接受一個(gè)函數(shù)名和另一個(gè)對(duì)象作為參數(shù)励稳;他會(huì)展開(kāi)成一個(gè)雙參數(shù)的函數(shù)調(diào)用佃乘,兩個(gè)對(duì)象都是被引用的。
我們?cè)趏bject 之前放上一個(gè)逗號(hào)的原因是驹尼,我們希望變量的值被插入進(jìn)反引號(hào)生成的列表里趣避。如果我們拿掉引號(hào),宏展開(kāi)就會(huì)變成(CONS AARDVARK AARDVARK)新翎,這會(huì)引起一個(gè)未賦值變量錯(cuò)誤程帕,除非AARDVARK本身具有一個(gè)值住练。如果我們拿掉逗號(hào),宏就會(huì)展開(kāi)成(CONS ’OBJECT ’OBJECT).
我們來(lái)嘗試一個(gè)更加發(fā)雜的反引號(hào)模板骆捧,我們會(huì)寫(xiě)一個(gè)宏showvar來(lái)展示變量的值澎羞。
showvar必須定義成宏的原因是,他必須知道要展示變量的名字敛苇,不僅僅是知道變量的值。讓我們來(lái)把問(wèn)題細(xì)化一下顺呕。關(guān)于X的值的信息會(huì)在接下來(lái)的表達(dá)式中被打印出來(lái)枫攀,請(qǐng)注意只由X的第一個(gè)實(shí)例被引用了。
我們現(xiàn)在可以簡(jiǎn)單的將showvar宏需要的部分抽象出來(lái)株茶。引號(hào)和逗號(hào)的組合看上去是有點(diǎn)奇怪来涨,但是你可以看看在接下來(lái)的例子中引號(hào)的位置在哪里。
14.7 用反引號(hào)連接
反引號(hào)的另一個(gè)特性就是如果一個(gè)模板元素的前面是一個(gè)逗號(hào)(,)和一個(gè)at符號(hào)(@)启盛,這個(gè)元素的值就會(huì)被連接到反引號(hào)構(gòu)造的結(jié)果中蹦掐,而不是插入進(jìn)去。(元素的值必須是一個(gè)列表)僵闯。如果只有一個(gè)逗號(hào)被使用的話卧抗,這個(gè)元素會(huì)作為一個(gè)單個(gè)對(duì)象被插入,導(dǎo)致一個(gè)附加的括號(hào)層次鳖粟。
下面表示的在哪里連接的例子是很有用的社裆,宏函數(shù)set-zero,接受任意數(shù)量的變量作為輸入向图,他會(huì)展開(kāi)成一個(gè)表達(dá)式來(lái)設(shè)置每一個(gè)變量的值泳秀,為0,然后返回一個(gè)消息作為結(jié)果榄攀。因?yàn)楹瓯仨殞?dǎo)致一些行為但是也可以只是返回一個(gè)值嗜傅,他用progn將一些行為組合進(jìn)單個(gè)表達(dá)式里面。
這就是set-zero的定義檩赢,它使用mapcar來(lái)給每一個(gè)參數(shù)列表中變量構(gòu)建一個(gè)setf表達(dá)式吕嘀。setf表達(dá)式之后就會(huì)連接在progn函數(shù)的后面。還有漠畜,在progn函數(shù)體中的最后一個(gè)表達(dá)式是一個(gè)用連接構(gòu)造的引用的列表币他。如果只是一個(gè)逗號(hào),而不是一個(gè)逗號(hào)個(gè)at符號(hào)的組合的haunted憔狞,結(jié)果就是(ZEROED (A B C))蝴悉。
14.8 編譯器
編譯器將lisp程序翻譯成機(jī)器語(yǔ)言。這會(huì)使得程序運(yùn)行的更快瘾敢,一般是快10到100倍拍冠。作為一個(gè)初學(xué)者你可能不會(huì)寫(xiě)一個(gè)很大規(guī)模的程序尿这,所以速度并不是首要的考慮因素。然而庆杜,當(dāng)你著手處理更加高層次的問(wèn)題射众,你就會(huì)開(kāi)始關(guān)心程序的速度和內(nèi)存占用率等等了。編譯就可以優(yōu)化這兩個(gè)因素晃财。
有兩種方式可以使用編譯器叨橱,你可以用compile函數(shù)編譯一個(gè)函數(shù),揮著使用compile-file來(lái)編譯整個(gè)文件断盛。很多面向lisp的編輯器都提供只用一次或者兩次擊鍵的方式來(lái)調(diào)用編譯器罗洗,所以你可能永遠(yuǎn)都不會(huì)有機(jī)會(huì)顯式調(diào)用這些函數(shù)了。
我們來(lái)看一下compile函數(shù)在運(yùn)行時(shí)編譯一個(gè)簡(jiǎn)單函數(shù)的結(jié)果钢猛。函數(shù)返回大于輸入的平方最小整數(shù)伙菜。這個(gè)函數(shù)以一種非常冗長(zhǎng)的方式計(jì)算出了結(jié)果,但是編譯會(huì)讓速度優(yōu)化很多命迈。
我們看到五百萬(wàn)的平方根是在2236到2237之間贩绕。我們也看到tedious-sqrt的解釋版本花費(fèi)了大約0.95秒鐘來(lái)運(yùn)行。你可能想要選一個(gè)更加小的參數(shù)壶愤,如果你的機(jī)器是比較慢的話∈缜悖現(xiàn)在,讓我們嘗試編譯tedious-sqrt來(lái)看看編譯版本有多快公你。
編譯版本滑了0.3125秒運(yùn)行時(shí)間踊淳,比解釋版本快30倍。解釋版本占用了查過(guò)50K字節(jié)陕靠,但是編譯版本只用了32字節(jié)迂尝。在特定的實(shí)現(xiàn)中,占用是因?yàn)?amp;rest在收集參數(shù)的時(shí)候?qū)?em>函數(shù)的使用剪芥。每次生成一個(gè)列表這個(gè)函數(shù)都會(huì)被調(diào)用垄开。編譯器會(huì)把對(duì)函數(shù)的調(diào)用編譯成機(jī)器指令,之后就抵消了函數(shù)調(diào)用所造成的損耗税肪。
14.9 編譯和宏展開(kāi)
common lisp標(biāo)準(zhǔn)允許宏調(diào)用被他們的宏展開(kāi)在任何時(shí)候替換溉躲。在一些lisp實(shí)現(xiàn)中defunct函數(shù)會(huì)進(jìn)行宏展開(kāi),在其他實(shí)現(xiàn)中第一次替換是會(huì)被求值的益兄。在簡(jiǎn)單的實(shí)現(xiàn)中一個(gè)宏調(diào)用可能一直不會(huì)被結(jié)果展開(kāi)給代替锻梳。而宏會(huì)每一次再展開(kāi)然后求值。
既然宏展開(kāi)會(huì)在任何時(shí)候發(fā)生净捅,你不應(yīng)該寫(xiě)一個(gè)產(chǎn)生副作用的宏疑枯,比如賦值或者i/o。但是宏展開(kāi)成產(chǎn)生副作用的表達(dá)式是可以的蛔六。
在上面的例子中荆永,宏被展開(kāi)成編譯say-hi函數(shù)的一部分過(guò)程废亭,所以編譯器會(huì)說(shuō)“hi呆贿,mom”髓削,宏的計(jì)算結(jié)果是nil,效果是被編譯到say-hi函數(shù)體中的表達(dá)式脸秽,沒(méi)有任何表示是因?yàn)楹暌呀?jīng)被表達(dá)式替換了骂删。問(wèn)題可以通過(guò)制造返回format的表達(dá)式來(lái)解決掌动。
14.10 編譯整個(gè)程序
當(dāng)你編譯整個(gè)程序,他會(huì)被存儲(chǔ)在一個(gè)文件里面桃漾。你可以適應(yīng)compile-file函數(shù)操作這個(gè)文件坏匪。一些lisp編輯器允許你使用編輯器命令來(lái)做這件事。他們可能也允許你去編譯一個(gè)編輯器緩沖的內(nèi)容而不需要他是一個(gè)文件撬统。請(qǐng)查閱你的用戶手冊(cè)。
由于編譯器的工作方式敦迄,你可能需要在阻止你的程序的時(shí)候遵守一些簡(jiǎn)單的規(guī)矩恋追。如果你不注意這些規(guī)則的話,編譯器可能會(huì)爆出錯(cuò)誤信息然后不再正確編譯你的程序罚屋。
首先苦囱,如果你的程序使用了任何全局變量,編譯器可能會(huì)爆出一個(gè)警告信息脾猛,說(shuō)變量是“假定為特殊變量(assumed to special)”撕彤。特殊變量將會(huì)在進(jìn)階話題中解釋,你可以忽視警告信息猛拴,使用defvar重新聲明變量羹铅。
第二,如果你的程序包含了宏愉昆,宏定義必須是直接存在與文件中职员。如果函數(shù)foo調(diào)用一個(gè)宏bar,lisp可能無(wú)法認(rèn)識(shí)到當(dāng)編譯foo的時(shí)候需要將bar的調(diào)用看做一個(gè)宏調(diào)用來(lái)展開(kāi)跛溉。如果foo已經(jīng)被不正確的編譯了焊切,發(fā)現(xiàn)bar是一個(gè)宏的時(shí)候,大部分編譯器將會(huì)報(bào)出警告芳室。
第三专肪,如果你的程序沖定義了任何內(nèi)建函數(shù),編譯器不會(huì)正確編譯他們堪侯。確保使用和內(nèi)建函數(shù)不沖突的名字嚎尤。在線文檔會(huì)幫助你來(lái)判斷。
14.11 案例學(xué)習(xí):有限狀態(tài)機(jī)
有限狀態(tài)機(jī)(Finite state machine FSM)是一種來(lái)自理論計(jì)算機(jī)技術(shù)的抖格,用來(lái)描述一種簡(jiǎn)單的機(jī)器诺苹,比如售貨機(jī)或者交通指示燈工作狀態(tài)的技術(shù)咕晋。在這一節(jié)我們會(huì)寫(xiě)一個(gè)有限狀態(tài)機(jī)模擬器來(lái)顯示真實(shí)的lisp程序是如何被開(kāi)發(fā)出來(lái)的。為了討論的更加具體一些收奔,我們會(huì)聚焦在一個(gè)特定的機(jī)器來(lái)進(jìn)行模擬掌呜,但是我們的虛擬機(jī)將會(huì)為任何有限狀態(tài)機(jī)工作。
假設(shè)一種售貨機(jī)只出售兩種商品:口香糖和薄荷糖坪哄≈式叮口香糖要15美分,薄荷糖要20美分翩肌。任何5分和10分的組合可能被用來(lái)操作這個(gè)機(jī)器模暗;他會(huì)自動(dòng)改變組合。如果提供了足夠的錢念祭,按下相應(yīng)鍵會(huì)出現(xiàn)相應(yīng)的商品兑宇,任何時(shí)候按下退款鍵就會(huì)返回剩下的錢。
這個(gè)機(jī)器一開(kāi)始初始化成一個(gè)狀態(tài)叫做start粱坤,如果輸入的是字符nickel隶糕,他會(huì)打印“clunk”并且轉(zhuǎn)移到叫做have-5的狀態(tài)。如果實(shí)在have-5的狀態(tài)并且取得了dime作為輸入站玄,就會(huì)打印“clink”,然后轉(zhuǎn)入狀態(tài)have-15枚驻。在have-15狀態(tài)如果輸入gum-button,就會(huì)輸出一包口香糖(gum),然后進(jìn)入狀態(tài)end株旷。
狀態(tài)機(jī)總共有六個(gè)狀態(tài):start,have-5,have-10,have-15,和have-20再登,還有end。(之所以被稱作有限狀態(tài)機(jī)的原因就是他的狀態(tài)時(shí)有限的)晾剖,每一個(gè)狀態(tài)都表現(xiàn)為一個(gè)節(jié)點(diǎn)锉矢,從一個(gè)狀態(tài)到另一個(gè)狀態(tài)之間的轉(zhuǎn)換都是一個(gè)箭頭。狀態(tài)都需要標(biāo)記成一個(gè)一個(gè)轉(zhuǎn)換的機(jī)器動(dòng)作钞瀑,例如沈撞,從have-10到have-15的轉(zhuǎn)換就被標(biāo)記為nickel。
在售貨機(jī)的全部定義中雕什,defnode宏和defarc宏提供了簡(jiǎn)單的語(yǔ)法來(lái)定義有限狀態(tài)機(jī)一部分缠俺。下面有限狀態(tài)機(jī)模擬器中你可以看到工作的模式和目標(biāo)。
從定義節(jié)點(diǎn)和狀態(tài)的結(jié)構(gòu)體開(kāi)始贷岸,我們來(lái)構(gòu)造我們虛擬機(jī)壹士。,誒一個(gè)節(jié)點(diǎn)都有自己的名字偿警,自己狀態(tài)輸入的列表躏救,還有輸出狀態(tài)的列表。每一個(gè)狀態(tài)都有一個(gè)from節(jié)點(diǎn)和一個(gè)to節(jié)點(diǎn),一個(gè)標(biāo)記和一個(gè)動(dòng)作盒使。我們也定義了打印函數(shù)崩掘。
現(xiàn)在我們需要一個(gè)全局變量nodes來(lái)保存包含機(jī)器節(jié)點(diǎn)的列表,還有全局變量arc來(lái)保存狀態(tài)的列表少办。另一個(gè)變量苞慢,“current-node”,來(lái)保存機(jī)器狀態(tài)的記錄英妓。用defvar來(lái)聲明這些變量挽放,哦我忙呢會(huì)在進(jìn)階話題里討論這個(gè)。函數(shù)initilize將這些變量設(shè)置成nil蔓纠。
宏defnode是用來(lái)定義新節(jié)點(diǎn)的狀態(tài)包裹(syntactic sugar)辑畦。他在參數(shù)前面加上一個(gè)引號(hào)來(lái)調(diào)用addnode函數(shù)。
add-node構(gòu)造一個(gè)新的節(jié)點(diǎn)腿倚,給這個(gè)節(jié)點(diǎn)一個(gè)名字纯出,把這個(gè)節(jié)點(diǎn)加入到全局變量nodes中。因?yàn)樗褂胣conc(append的破壞性版本)所以可以再列表的最后加上一個(gè)節(jié)點(diǎn)敷燎。這確保了在nodes中的幾點(diǎn)會(huì)以defnode定義的順序出現(xiàn)潦刃。而不是相反的順序。add-node也會(huì)返回新建的節(jié)點(diǎn)懈叹。
FIND-NODE函數(shù)接受一個(gè)節(jié)點(diǎn)的名字作為輸入,然后返回一個(gè)對(duì)應(yīng)的節(jié)點(diǎn)如果對(duì)應(yīng)節(jié)點(diǎn)不存在分扎,那么就會(huì)報(bào)錯(cuò) 澄成。
宏defarc提供了一個(gè)方便的定義狀態(tài)的語(yǔ)法,并且函數(shù)add-arc做一些實(shí)質(zhì)上的工作畏吓。當(dāng)一個(gè)狀態(tài)唄創(chuàng)建墨状,他被加入node-ooutputs列表還有node-inputs列表。也不會(huì)被加入保存全局變量arcs的列表菲饼。
現(xiàn)在我們可以寫(xiě)一個(gè)頂層函數(shù)fsm肾砂。他會(huì)接受一個(gè)可選的輸入來(lái)定義狀態(tài)機(jī)的初始狀態(tài)。默認(rèn)的初始狀態(tài)是start宏悦。fsm反復(fù)調(diào)用函數(shù)one-transtation來(lái)轉(zhuǎn)移到下一個(gè)狀態(tài)镐确。當(dāng)狀態(tài)機(jī)到達(dá)一個(gè)沒(méi)有輸出的狀態(tài)(比如end),就會(huì)停止饼煞,請(qǐng)注意do是沒(méi)有一個(gè)空的變量列表的源葫。
最后,我們來(lái)寫(xiě)一個(gè)one-transition函數(shù)砖瞧。他會(huì)提示輸入并且通過(guò)改變current-node的值來(lái)創(chuàng)建合適的狀態(tài)息堂。如果從輸入無(wú)法給出一個(gè)合法的狀態(tài)的話,就會(huì)打印一個(gè)錯(cuò)誤消息并且提示再一次輸入。
我們的模擬器不僅僅限制在模擬售貨機(jī)荣堰。任何可以被描述為有限的狀態(tài)床未,而且狀態(tài)可以互相轉(zhuǎn)換的設(shè)備都可以用這個(gè)程序模擬。
小結(jié)
宏是一種很有用處的lisp縮略法振坚。它允許程序員來(lái)定義lisp的語(yǔ)法擴(kuò)展薇搁,并且將功能實(shí)現(xiàn)的更加精煉。他也會(huì)幫助供應(yīng)商向消費(fèi)者隱藏大量細(xì)節(jié)的實(shí)現(xiàn)定義屡拨。宏不會(huì)對(duì)參數(shù)求值只酥;他們返回會(huì)被lisp求值的表達(dá)式。新的宏可以被defmarco定義呀狼。
就像宏一樣裂允,特殊函數(shù)不會(huì)對(duì)輸入求值。但是和宏不同的是哥艇,他們不會(huì)返回lisp表達(dá)式绝编。特殊函數(shù)提供lisp創(chuàng)建的原始函數(shù),比如賦值貌踏,條件是和塊結(jié)構(gòu)十饥。
反引號(hào)字符從模板中構(gòu)造一個(gè)列表。如果模板元素的前面是一個(gè)逗號(hào)祖乳,他就會(huì)被求值逗堵。也就是會(huì)被插入進(jìn)列表的值。前導(dǎo)逗號(hào)和at符號(hào)的元素會(huì)被連接在列表的后面眷昆。反引號(hào)在宏中式特別有用的蜒秤,可以在模板中加入空格來(lái)構(gòu)造特別復(fù)雜的表達(dá)式。
本章涉及函數(shù)
宏定義:DEFMACRO.
編譯器: COMPILE, COMPILE-FILE.
Lisp Toolkit: PPMX
ppmx是pretty print marco expansion的縮寫(xiě)亚斋。他的宏展開(kāi)第一個(gè)參數(shù)(不求值)并且打印其余的參數(shù)作媚。ppmx不僅對(duì)學(xué)習(xí)內(nèi)建宏,諸如setf十分有用帅刊,也會(huì)對(duì)調(diào)試自己寫(xiě)的宏有幫助纸泡,如果在展開(kāi)的時(shí)候有什么問(wèn)題的話。
如果一個(gè)宏展開(kāi)成另一個(gè)宏調(diào)用赖瞒,ppmx會(huì)展示兩者的結(jié)果女揭,第一個(gè)表達(dá)式和最后一個(gè)表達(dá)式推導(dǎo)的,當(dāng)所喲的表達(dá)式都被展開(kāi)的時(shí)候冒黑。例如田绑,宏length-incf會(huì)展開(kāi)成一個(gè)setf宏的調(diào)用。setf再展開(kāi)成一個(gè)setq的特殊函數(shù)抡爹。
在一些實(shí)現(xiàn)中掩驱,dotimes宏展開(kāi)成一個(gè)do宏的調(diào)用。在下面的例子中,do依次展開(kāi)成一個(gè)更加復(fù)雜的表達(dá)式欧穴,包括block民逼,let,tagbody和go涮帘。在本書(shū)中拼苍,我們不會(huì)討論tag的函數(shù)體和go。
第十四章進(jìn)階話題
14.12 lambda列表關(guān)鍵字&BODY
人們寫(xiě)宏的理由之一是他可以給lisp加上新的語(yǔ)法调缨。例如疮鲫,我們可以寫(xiě)一個(gè)while宏來(lái)提供和在其他語(yǔ)言中一樣的控制結(jié)構(gòu)。
while宏接受一個(gè)測(cè)試表達(dá)式作為第一個(gè)參數(shù)弦叶,如果測(cè)試為真俊犯,他之后的函數(shù)體表達(dá)式會(huì)被求值。函數(shù)體表達(dá)式會(huì)被關(guān)鍵字&rest手機(jī)伤哺,但是common lisp包含一個(gè)特殊的關(guān)鍵字燕侠,&body,用在剩下的參數(shù)是從一些控制結(jié)構(gòu)中提取的宏立莉。一些lisp編輯器將注意力放在關(guān)鍵字切分調(diào)用成宏的階段绢彤。&body關(guān)鍵字的使用也是意味著宏定義的讀者可已經(jīng)剩下的參數(shù)作為lisp代碼的函數(shù)體。
函數(shù)NEXT-POWER-OF-TWO使用while循環(huán)來(lái)反復(fù)加倍變量I的值蜓耻,從1開(kāi)始茫舶,知道第一個(gè)兩倍數(shù)大于輸入的值n。
在最好的風(fēng)格中刹淌,特定的額問(wèn)題應(yīng)該使用do而不是while來(lái)解決奇适,這是要避免顯式的setf調(diào)用。
14.13 破壞lambda列表
宏MIX-AND-MATCH接受連個(gè)對(duì)偶(pair)作為輸入然后返回生成四對(duì)的表達(dá)式芦鳍。
在上面的例子中,我們手動(dòng)輸入了(FRED WILMA)和(BARNEY BETTY)葛账。但是既然宏是不對(duì)自己的參數(shù)氣質(zhì)的柠衅,他們就可以將輸入的表達(dá)式自動(dòng)作為列表結(jié)構(gòu)的一部分。這就是我們已知的破壞性籍琳。你可以定義如何破壞一個(gè)表達(dá)式菲宴,通過(guò)使用另一個(gè)整個(gè)參數(shù)列表替換宏參數(shù)列表的變量來(lái)實(shí)現(xiàn)。例如趋急,我們可以用(x1 y1)替換MIX-AND-MATCH中的p 喝峦,還有(x2 y2)來(lái)替換。之后的版本就是使用破壞性的呜达。
破壞性值在宏之中可以使用谣蠢,在普通函數(shù)中不可以。對(duì)于宏來(lái)說(shuō),定義新的復(fù)雜語(yǔ)法的控制結(jié)構(gòu)的視乎眉踱,破壞性就和有用了挤忙。宏dovector接下來(lái)就是模仿dotimes和dlist了。后續(xù)的步驟就是步進(jìn)向量中的元素了谈喳。宏使用破滑行的手段來(lái)提取向量中的索引變量册烈,向量表達(dá)式和結(jié)果形式。
你可以從dovector的展開(kāi)中看出婿禽,為什么這個(gè)宏是一種很有用的縮略法赏僧。
dovector使用本地變量vec-dov(儲(chǔ)存向量)和len-dov(儲(chǔ)存長(zhǎng)度),還有索引變量i-dov來(lái)展開(kāi)成一個(gè)do表達(dá)式扭倾。選擇這些名字是因?yàn)樗麄儾粫?huì)與用戶的任何變量產(chǎn)生沖突淀零。如果我們使用了vec,len和i吆录,作為變量名窑滞,可以防止用戶訪問(wèn)一些他們自己的變量。展開(kāi)在do*的函數(shù)體中也會(huì)包含變量x的顯式賦值恢筝,更深層次的lisp展開(kāi)是由block哀卫,lettagbody和go組成的,這個(gè)dovector表達(dá)式對(duì)于人類閱讀來(lái)說(shuō)更加合適一些撬槽。
14.14 宏和常量作用域(lexically scoping)
我們回到對(duì)于faulty-incf的觀察此改,一個(gè)incf的實(shí)現(xiàn)嘗試,作為一個(gè)函數(shù)而不是一個(gè)宏侄柔。假設(shè)我們?cè)趥鹘o函數(shù)之前就引用變量共啃,(FAULTY-INCF ’A)。faulty-incf需要做兩件事暂题,必須找出變量當(dāng)前的值移剪,還有就是替換原來(lái)的值。
對(duì)于全局變量也是可以的薪者,回憶一下鉆具變量在字符的值單元的命名方式纵苛。我們可以使用內(nèi)建函數(shù)symbol-value來(lái)訪問(wèn)值單元,我們用setf或者內(nèi)建的set函數(shù)可以存儲(chǔ)值言津。
函數(shù)看上去工作是很正確的攻人,但是也只是對(duì)全局變量有效。如果我們嘗試使用在本地變量上悬槽,就會(huì)失敗怀吻。simple-incf不論是本地變量或者全局變量都可以使用。
在test-simple中宏simple-incf被展開(kāi)成一個(gè)時(shí)候求值的表達(dá)式初婆,在test-simple的常量?jī)?nèi)容當(dāng)中蓬坡。所以本地變量turnp是一個(gè)常量猿棉,這是沒(méi)有問(wèn)題的。
在faulty-incf求值回溯圖中我們可以看到bug渣窜。在faulty-incf的函數(shù)體重铺根,只有本地變量var是可見(jiàn)的。粗實(shí)線包括的內(nèi)容是顯示了上層的turnip的常量?jī)?nèi)容乔宿。沒(méi)有值被賦予全局變量trunip是不可訪問(wèn)的位迂。所以當(dāng)symbol-value找到值單元的時(shí)候會(huì)有個(gè)未賦值錯(cuò)誤。
14.15 宏的歷史意義
相比普通函數(shù)和特殊函數(shù)详瑞,宏的特性之一就是語(yǔ)法是可定義的掂林。這回是的程序員可以對(duì)自己的語(yǔ)法擴(kuò)展定義有清晰地認(rèn)識(shí)。使用擴(kuò)展的人說(shuō)明他們是定義的而不是內(nèi)建的使用者坝橡。相比之下泻帮,像pascal這種語(yǔ)言的話是不可以加一個(gè)新的語(yǔ)句類型的。只有新的過(guò)程可以加计寇。擴(kuò)展pascal語(yǔ)法的唯一方式是寫(xiě)一個(gè)預(yù)處理器或者修改編譯器锣杂。兩個(gè)方法都是不可操作的。
common lisp的很多特性在早期的方言中都是有程序員以宏包的方式定義的番宁。例如setf和destruct元莫,還有with-open-file宏。甚至defmarco是原始的擴(kuò)展蝶押。(雖然lisp在一開(kāi)始已經(jīng)有宏了踱蠢,在defmarco出現(xiàn)前,還是使用一個(gè)和累贅的方法實(shí)現(xiàn)的棋电。)
在許許多多的人的聰明才智和辛勤輔助下茎截,lisp已經(jīng)持續(xù)演化了超過(guò)三十年了。這個(gè)進(jìn)化也包括了宏赶盔。除了擴(kuò)展lisp企锌,宏也可以被用在定義一種新的語(yǔ)言上。用于人工智能的很多高級(jí)語(yǔ)言都是建立在lisp的基礎(chǔ)上于未。本書(shū)中霎俩,我們用common lisp宏創(chuàng)造了一個(gè)特殊圖形語(yǔ)言實(shí)現(xiàn)。
14.16 動(dòng)態(tài)作用域
縱觀本書(shū)沉眶,我們已經(jīng)在所有變量上使用了語(yǔ)法作用域。語(yǔ)法作用與以為這對(duì)于函數(shù)foo就可以訪問(wèn)變量x杉适,foo的定義必須出現(xiàn)在x定義的范圍內(nèi)谎倔。如果foo定義在頂層的額haunted,只能訪問(wèn)全局變量(加上任何定義的本地變量)猿推,但是如果函數(shù)定義是一個(gè)lambda表達(dá)式的話片习,在另一個(gè)函數(shù)bar的函數(shù)體中出現(xiàn)的話捌肴,就可以訪問(wèn)abr的本地變量還有自己的。函數(shù)定義在bar外面就不能訪問(wèn)了藕咏。
可以選擇語(yǔ)法作用域的特定叫做動(dòng)態(tài)作用域状知,在common lisp之前,動(dòng)態(tài)作用域是lisp的常態(tài)配置孽查。語(yǔ)法作用域現(xiàn)在只在兩種方言中存在scheme和T饥悴。
動(dòng)態(tài)作用域變量也可以誒特殊函數(shù)調(diào)用,當(dāng)一個(gè)變量名被聲明為特殊的時(shí)候盲再,變量就不會(huì)是任何函數(shù)的本地變量西设,他的值可以再任何地方被訪問(wèn)。相比之下答朋,常量作用域變量是指在定義的函數(shù)體內(nèi)才可以訪問(wèn)的贷揽。聲明一個(gè)變量為特殊的方法是使用defvar宏。
我們來(lái)比較變量的兩種作用域的效果梦碗。我們生命bird是一個(gè)動(dòng)態(tài)作用域禽绪。我們會(huì)使用fish作為一個(gè)常量作用域變量,所以不應(yīng)該使用defvar來(lái)定義洪规。每一個(gè)變量都會(huì)被賦值初始化印屁,之后再寫(xiě)一個(gè)函數(shù)來(lái)引用每一個(gè)變量的值。
現(xiàn)在我們來(lái)看一下另種作用域的規(guī)則區(qū)別淹冰,我們會(huì)寫(xiě)一個(gè)佳作fish和birds的函數(shù)库车,首先我們看熟悉的,常量作用域的情況fish樱拴。
在test-lexical中柠衍,表達(dá)式fish指向本地變量fish。本地變量對(duì)ref-fish是不可見(jiàn)的晶乔。在ref-fish函數(shù)體中的字符fish仍然指向全局變量fish珍坊。在求值回溯圖中你可以看到ref-fish被實(shí)現(xiàn)包裹,顯示了上層語(yǔ)法環(huán)境是全局環(huán)境正罢。既然reffish不會(huì)創(chuàng)造一個(gè)自己的本地變量fish阵漏,任何出現(xiàn)fish的地方都是指向全局變量。值是(salmon tuna)翻具。
在動(dòng)態(tài)作用域的情況下履怯,使用birds,測(cè)試函數(shù)訊早前一個(gè)的定義裆泳,但是行為不同叹洲。區(qū)別在與defvar的效果是將birds定義為特殊的。
當(dāng)我們進(jìn)入test-dynamic的函數(shù)體中工禾,一個(gè)新的動(dòng)態(tài)變量birds就會(huì)被創(chuàng)建运提。從現(xiàn)在直到我們離開(kāi)函數(shù)體蝗柔,每一次使用birds就會(huì)指向這個(gè)變量,甚至是在其他函數(shù)的函數(shù)體中民泵。全局變量birds在新的動(dòng)態(tài)變量存在的情況下就是不可訪問(wèn)的癣丧。當(dāng)test-dynamic返回的時(shí)候,動(dòng)態(tài)變量birds就會(huì)消失栈妆,還有同名的birds會(huì)再次綁定全局變量胁编。
對(duì)于動(dòng)態(tài)變量,沒(méi)有特殊的求值回溯標(biāo)記來(lái)表示签钩。你可以簡(jiǎn)單記住一些已經(jīng)被defvar定義的名字掏呼,一旦這樣這樣做,所有的變量birds都會(huì)指向動(dòng)態(tài)作用域铅檩。值就是(eagle vulture)憎夷。
動(dòng)態(tài)作用域的求值規(guī)則是,如果遇到了粗實(shí)線不是指向全局變量的作用域昧旨,而是直接傳遞拾给,繼續(xù)尋找創(chuàng)建的變量名。如果只想愛(ài)你個(gè)了全局環(huán)境兔沃,意味著沒(méi)有函數(shù)現(xiàn)在有一個(gè)這個(gè)名字的額全局變量蒋得,我們只用全局的值。
術(shù)語(yǔ)動(dòng)態(tài)綁定的意思就是指在ref-birds里的變量birds沒(méi)有永久綁定在一個(gè)變量上乒疏,fish在ref-fish中關(guān)聯(lián)在一個(gè)全局變量上额衙。也就是說(shuō),名字和真是變量的列檢是動(dòng)態(tài)的怕吴。當(dāng)ref-birds在test-dynamic內(nèi)部被調(diào)用窍侧,字符birds指向由test-dynamic建立的動(dòng)態(tài)變量。當(dāng)ref-birds在頂層被調(diào)用转绷,同樣的字符birds是被解釋為全局變量的引用伟件。
動(dòng)態(tài)作用域應(yīng)該謹(jǐn)慎使用,在早期的lisp方言中他是默認(rèn)的议经,這引起了很多程序bug的出現(xiàn)斧账,一個(gè)程序意外修改了一個(gè)另一個(gè)程序創(chuàng)建的動(dòng)態(tài)變量。語(yǔ)法作用域保護(hù)一個(gè)函數(shù)的本地變量不會(huì)被其他無(wú)關(guān)函數(shù)修改煞肾。但是也有一些環(huán)境咧织,動(dòng)態(tài)作用是不二之選。
14.17 DEFVAR, DEFPARAMETER, DEFCONSTANT
DEFVAR, DEFPARAMETER, 和DEFCONSTANT都是聲明名字是特殊的函數(shù)籍救。defvar被用來(lái)聲明變量的值會(huì)在程序的正常操作中改變习绢,他會(huì)接受一個(gè)可循啊的初始值,和一個(gè)文檔字符串钧忽。
一個(gè)有意思的事實(shí)是defvar是不會(huì)改變已經(jīng)由值的變量的毯炮。只會(huì)賦值那些沒(méi)有值的變量。
DEFPARAMETER和DEFVAR的語(yǔ)法相同,但是會(huì)被用在聲明值不會(huì)被程序運(yùn)行時(shí)改變的變量上耸黑。他們有參數(shù)設(shè)定的能力桃煎,也就是告訴程序如何行為。另一個(gè)區(qū)別就是大刊。DEFPARAMETER會(huì)給已經(jīng)有值的變量賦值为迈。
備用咋定義常量,也就是值絕對(duì)不會(huì)被改變缺菌。在Lisp中的慣例是使用星號(hào)包裹變量名葫辐,但是這個(gè)而不是用在常量上的。嘗試改變一個(gè)常量的值就會(huì)報(bào)錯(cuò)伴郁,或者常在一個(gè)相同名字的新的變量作為常量耿战。PI就是一個(gè)內(nèi)建的常量。
如果是一個(gè)變量的話焊傅,聲明一個(gè)變量為常量有時(shí)候允許編譯器生成更加有效率的機(jī)器語(yǔ)言剂陡,也會(huì)防止意外的改變變量的值。大部分實(shí)現(xiàn)仍然允許你故意改變值狐胎,雖然是在調(diào)試器當(dāng)中鸭栖。
14.18 重綁定特殊變量
很多動(dòng)態(tài)作用域時(shí)代的lisp變量術(shù)語(yǔ)對(duì)今天來(lái)說(shuō)是過(guò)時(shí)的,由于歷史原因握巢,一些作者在談?wù)搫?chuàng)建變量的時(shí)候是說(shuō)綁定一個(gè)變量的晕鹊。也說(shuō):“未綁定變量”,在說(shuō)”未賦值變量“的時(shí)候暴浦。綁定不是一定指賦值溅话,這是lisp術(shù)語(yǔ)系統(tǒng)中的一大迷惑的地方。非全局變量總是有值的肉渴,但是全局變量和特殊變量也有可能沒(méi)有值公荧。在本書(shū)中,我們不會(huì)談?wù)摵芑逎募?xì)節(jié)同规。
到現(xiàn)在為止我們已經(jīng)避免了因?yàn)槭褂媒壎◣?lái)的困擾循狰。在最后一節(jié)中我們會(huì)介紹術(shù)語(yǔ)重綁定來(lái)指向使用舊的名字來(lái)創(chuàng)造一個(gè)全新的變量。新的變量的存在的那個(gè)時(shí)候券勺,在程序的任何地方這個(gè)名字都會(huì)指向這個(gè)變量绪钥。之前的那個(gè)變量就會(huì)不能使用。嚴(yán)格來(lái)說(shuō)关炼,我們不會(huì)重綁定任何變量程腹,我們動(dòng)態(tài)綁定名字,暫時(shí)指向不同的變量儒拂。
common lisp包含了很多內(nèi)建的特殊變量寸潦,一些事輸入輸出的時(shí)候使用的色鸳。例如,變量print-base就是format函數(shù)和其他函數(shù)決定那個(gè)數(shù)值來(lái)打印的變量见转。既然已經(jīng)定義為特殊命雀,我們就不是一定要使用defvar。為了重綁定他斩箫,我們僅僅包括在我們函數(shù)的參數(shù)列表中吏砂。
我們也可以使用let來(lái)重綁定變量。當(dāng)一個(gè)特殊變量被重綁定乘客,人格賦值狐血,無(wú)論在程序的何處,將會(huì)影響新的變量而不是舊的那個(gè)易核。在接下來(lái)的例子中匈织,當(dāng)bump-foo在let的函數(shù)體中被調(diào)用,他的增值的是let中建立的動(dòng)態(tài)變量foo耸成。當(dāng)他在let外部被調(diào)用报亩,他增值的就是全局變量foo。如果foo還沒(méi)有被聲明為特殊井氢,bump-over總是可以訪問(wèn)全局變量foo弦追。
當(dāng)大型程序的不同部分需要互相交換信息的時(shí)候,特殊變量的重綁定是最有用的花竞,并且想要通過(guò)附加參數(shù)給喊出傳遞變量是不可行的劲件。寫(xiě)一個(gè)真實(shí)的大型程序需要很多技能,比這本書(shū)所說(shuō)的多得多约急,這是一個(gè)進(jìn)階lisp課程的很棒的主題零远。
進(jìn)階話題涉及函數(shù)
DEFMACRO: lambda列表關(guān)鍵字&BODY
聲明: DEFVAR, DEFPARAMETER, DEFCONSTANT