第十四章 宏和編譯(Macros and Compilation)
14.1 導語
宏函數,縮略,宏。是擴展lisp語法的一種方式。在本章我們會使用求值回溯圖和一些叫做ppmx的小工具(定義在lisp toolkit章節)來看看宏是如何工作的。會有一些材料需要參考前面的進階話題章節。如果你還沒有看過的話,我會標明去哪里看的。
在本章的后半部分我們會學習一下編譯(compilation)。如果你覺得你的程序中有一部分運行的太慢,你可以將他們編譯一下來讓他快一些。編譯器將lisp程序翻譯成機器語言程序,這樣他的執行速度會有10倍到100倍的提升。
14.2 宏就是縮寫法
在計算機中的宏實際上就是縮寫法,任何你想要寫成所寫的東西都可以用更加詳細的語言來表示,只是會更長一些。相似的Common Lisp宏不會讓你寫出任何用一般函數不能表示的功能。但是他確實會幫你把功能寫的更加精煉。incf函數就是一個好例子,(incf a)肯定比(setf a (+ A 1))寫起來要精煉的多吧。
有一些宏是非常精妙的,特別是像setf和incf這種一般賦值宏,他可以將任意的復雜位置描述解釋成一般的變量引用。
當上面表達式里的位置描述被解析出來的時候,你會驚嘆incf的強大能力。
宏可以用簡單的指令生成復雜的程序。比如destruct宏,可以講starship的結構定義轉化成一個指令流來支持starship的數據類型。這個指令流包括了make-starshiphe starship-p的函數定義,還有所有組件的訪問函數。當然所有的工作并不只是包括函數定義,defstruct的輸出取決于具體實現的定義。例如,starship整合進Common Lisp類型繼承機構的方式就是每一個實現都是不同的。defstruct包含的函數和變量都不是Common Lisp標準的一部分,甚至不會被lisp供應商寫進文檔里。defstruct宏允許lisp供應商提供一個協議來對消費者隱藏具體細節,定義結構體的標準方式取決于具體的Common Lisp實現。
14.3 宏展開
如果你用縮略法寫了一些東西的話,為了便于理解和使用,你終歸是要把他展開的。lisp會把宏調用自動展開,一個宏實際上就是一種特殊的縮略法,展開宏函數的時候并不會對它的參數求值。展開的工作時尋找它的參數還有生成一個lisp可以求值的表達式。在表達式(incf a)中,宏incf被調用,參數就是A(不求值)。他會構建一個表達式,(setq a (+ A 1)),之后返回。incf準確構建的表達式取決于具體實現,但是看上去應該是setq。lisp之后會對表達式求值,然后對A的值加上1。
回憶一下我們在10.10章節講的特殊函數setq,當setf給一個普通變量賦值的時候,實際上是在內部調用setq函數。這個setf宏函數實際上是展開成了一個setq函數的調用。
在求值回溯圖中,宏展開使用一條虛線來表示,宏返回的表達式會被正常求值。
如果你想要看一下計算機內部的宏展開,你可以使用一個叫做ppmx的小工具,定義在本章節的lisptoolkit單元。ppmx這個名字是pretty print macro expansion(吐槽:漂亮滴打印展開啊喂!)的縮寫,一些lisp實現也會有打印展開的工具。
在一些lisp實現中,incf的宏展開是不一樣的,例如,incf可能會展開成一個let表達式來創造一個本地變量保存(+ A 1)的值,然后再重新傳遞給A。看上去這好像并不是一個直接的方法來給a加1.但是請記住,incf的目的是更加復雜的結構也能進行增值操作,在哪些情況下,let或許是一個更必要的選擇。
在上面的例子中,#:G0144是一個內部符號,叫做內生變量(gensym(查了一下,不是英語單詞,是造的詞,專家系統的意思)),是由incf函數自動生成的變量,作為一個本地變量名使用。內生變量確保不會和你已有的任何變量名產生沖突。出于我們不需要了解的理由,#:G0144和符號G0144是不同的,你不能從鍵盤輸入這樣的符號,所以他也不可能和你的變量名沖突,甚至你特意選擇G0144作為名字。
14.4 定義一個宏
宏是由defmarco來定義的,它的語法和defun類似。我們來定義一個給普通變量加上1的incf的簡單版本。我們紅會接受一個變量名作為輸入,冰鞋構造一個增量表達式。
另一個看simple-incf如何工作的方法就是用dtrace來追蹤他的行為。(如果你使用的標準的trace而不是dtrace的話罵你可能不能追蹤宏。)
追蹤你自己寫的宏是沒有問題的,但是在一些lisp實現當中最好不要追蹤那寫重要的內建函數,比如setf。如果追蹤這些函數出現了問題,換成ppmx試下。使用ppmx的好處之一就是宏展開的結果只是被打印,但是不會被求值。ppmx允許你使用任意表達式來嘗試宏展開而不用擔心會發生錯誤。
現在我們來修改一下simple-incf,可以接受第二個參數,定義增值的幅度。我們使用lambda列表關鍵字&optional來實現。(可選參數在章節11.13里有解釋過)對變量默認的增值數字是1。
宏不會對它的參數進行求值,所以simple-incf的輸入是字符b和列表(* 3 A),不是數字2和15.求值回溯圖展示了simple-incf如何計算出宏展開,之后lisp才求值。
現在我們來思考一下,為什么incf一定要是一個宏而不是一個函數呢?假設我們頂一個函數incf,用defun定義,就叫他faulty-incf好了:
既然faulty-incf是一個函數,那么他就會對它的參數求值,并且也沒有返回一個表達式給lisp求值的打算,僅僅是自己把事情給辦了。但是它的參數已經被求值了之后,就會出現一個問題。
faulty-incf的輸入是7,他創造了一個叫var的本地變量,然后將輸入存儲進去,之后再將var的值加上1.他對變量A根本就沒有概念,因為在函數被輸入之前,參數就應被求值了。
我們可能嘗試在變量a被傳給faulty-incf的時候給他加上一個引號,當然必須是要更改faulty-incf的定義,因為輸入不再是一個數字。但是因為某些理由(會在進階話題里解釋),這也是不起作用的。simple-incf必須被寫成一個宏,這并不和我們先前說的宏是縮略法的說法沖突。我們任然可以自由使用setq表達式來代替simple-incf。setq是一個特殊函數,不是一個宏,他們之間的區別會在下一節解釋。
14.5 宏是語法擴展
既然宏的目的是擴展語言本身的語法,lisp就不會像對待普通調用那樣對待宏調用。在普通調用和宏調用之間有三個重要區別。
- 普通函數的參數請示被求值的,宏函數的參數不被求值。
- 普通函數的結果可以使任何東西,宏函數返回的結果肯定是一個合法的lisp表達式。
- 宏函數返回一個表達式之后,表達式是馬上被求值的,普通函數返回的結果是不被求值的。
除了宏之外,common lisp還包括了一部分特殊函數,比如說setq,if,let,和block。特殊函數是構建Common Lisp的最底層模塊,他們主要負責像賦值,作用域,基本控制結構,塊和循環。類似于宏,特殊函數并不對他們的參數求值,但是他們也不返回被求值的表達式。他們是做很特殊的事情的原始函數。你不能寫一個新的特殊函數,只有Lisp實現者才可以。
回到我們關于宏就是縮略法的討論。我們會說任何宏可以做到的事情,沒有了宏只用普通函數和特殊函數等等的組合也可以做的到。
14.6 反引號字符
函數simple-incf通過兩個list調用,還有一些引號字符,加上變量var和amoun的值構造了一個lisp表達式。在表達式規模比較小的時候,這個方法是可以的,但是當宏函數必須生成龐大復雜的表達式的時候,一個個構造就顯得很麻煩了。我們所需要的是一個替代的方法來實現寫一個表達式宏的模板。止嘔所有的宏都必須由空格填滿,反引號就是起那個作用。
反引號字符(`)和引號是相似的,他們都被用在引號列表中。然而,在一個反引號列表中,前導逗號的存在作為表達式沒有引用的標記,意思就是表達式的值而不是表達式會被使用。
我們可以使用反引號表達式來寫一個simpleincf的更加精細的版本。
宏的一個很普遍的用法就是去避免引用參數。宏使用需要的參數的引用版本來展開成一個普通函數調用。你可以使用反引號去生成表達式,并且在其間夾雜引號,如下面的模板。
在下面的例子中,two-from-one宏函數接受一個函數名和另一個對象作為參數;他會展開成一個雙參數的函數調用,兩個對象都是被引用的。
我們在object 之前放上一個逗號的原因是,我們希望變量的值被插入進反引號生成的列表里。如果我們拿掉引號,宏展開就會變成(CONS AARDVARK AARDVARK),這會引起一個未賦值變量錯誤,除非AARDVARK本身具有一個值。如果我們拿掉逗號,宏就會展開成(CONS ’OBJECT ’OBJECT).
我們來嘗試一個更加發雜的反引號模板,我們會寫一個宏showvar來展示變量的值。
showvar必須定義成宏的原因是,他必須知道要展示變量的名字,不僅僅是知道變量的值。讓我們來把問題細化一下。關于X的值的信息會在接下來的表達式中被打印出來,請注意只由X的第一個實例被引用了。
我們現在可以簡單的將showvar宏需要的部分抽象出來。引號和逗號的組合看上去是有點奇怪,但是你可以看看在接下來的例子中引號的位置在哪里。
14.7 用反引號連接
反引號的另一個特性就是如果一個模板元素的前面是一個逗號(,)和一個at符號(@),這個元素的值就會被連接到反引號構造的結果中,而不是插入進去。(元素的值必須是一個列表)。如果只有一個逗號被使用的話,這個元素會作為一個單個對象被插入,導致一個附加的括號層次。
下面表示的在哪里連接的例子是很有用的,宏函數set-zero,接受任意數量的變量作為輸入,他會展開成一個表達式來設置每一個變量的值,為0,然后返回一個消息作為結果。因為宏必須導致一些行為但是也可以只是返回一個值,他用progn將一些行為組合進單個表達式里面。
這就是set-zero的定義,它使用mapcar來給每一個參數列表中變量構建一個setf表達式。setf表達式之后就會連接在progn函數的后面。還有,在progn函數體中的最后一個表達式是一個用連接構造的引用的列表。如果只是一個逗號,而不是一個逗號個at符號的組合的haunted,結果就是(ZEROED (A B C))。
14.8 編譯器
編譯器將lisp程序翻譯成機器語言。這會使得程序運行的更快,一般是快10到100倍。作為一個初學者你可能不會寫一個很大規模的程序,所以速度并不是首要的考慮因素。然而,當你著手處理更加高層次的問題,你就會開始關心程序的速度和內存占用率等等了。編譯就可以優化這兩個因素。
有兩種方式可以使用編譯器,你可以用compile函數編譯一個函數,揮著使用compile-file來編譯整個文件。很多面向lisp的編輯器都提供只用一次或者兩次擊鍵的方式來調用編譯器,所以你可能永遠都不會有機會顯式調用這些函數了。
我們來看一下compile函數在運行時編譯一個簡單函數的結果。函數返回大于輸入的平方最小整數。這個函數以一種非常冗長的方式計算出了結果,但是編譯會讓速度優化很多。
我們看到五百萬的平方根是在2236到2237之間。我們也看到tedious-sqrt的解釋版本花費了大約0.95秒鐘來運行。你可能想要選一個更加小的參數,如果你的機器是比較慢的話。現在,讓我們嘗試編譯tedious-sqrt來看看編譯版本有多快。
編譯版本滑了0.3125秒運行時間,比解釋版本快30倍。解釋版本占用了查過50K字節,但是編譯版本只用了32字節。在特定的實現中,占用是因為&rest在收集參數的時候對函數的使用。每次生成一個列表這個函數都會被調用。編譯器會把對函數的調用編譯成機器指令,之后就抵消了函數調用所造成的損耗。
14.9 編譯和宏展開
common lisp標準允許宏調用被他們的宏展開在任何時候替換。在一些lisp實現中defunct函數會進行宏展開,在其他實現中第一次替換是會被求值的。在簡單的實現中一個宏調用可能一直不會被結果展開給代替。而宏會每一次再展開然后求值。
既然宏展開會在任何時候發生,你不應該寫一個產生副作用的宏,比如賦值或者i/o。但是宏展開成產生副作用的表達式是可以的。
在上面的例子中,宏被展開成編譯say-hi函數的一部分過程,所以編譯器會說“hi,mom”,宏的計算結果是nil,效果是被編譯到say-hi函數體中的表達式,沒有任何表示是因為宏已經被表達式替換了。問題可以通過制造返回format的表達式來解決。
14.10 編譯整個程序
當你編譯整個程序,他會被存儲在一個文件里面。你可以適應compile-file函數操作這個文件。一些lisp編輯器允許你使用編輯器命令來做這件事。他們可能也允許你去編譯一個編輯器緩沖的內容而不需要他是一個文件。請查閱你的用戶手冊。
由于編譯器的工作方式,你可能需要在阻止你的程序的時候遵守一些簡單的規矩。如果你不注意這些規則的話,編譯器可能會爆出錯誤信息然后不再正確編譯你的程序。
首先,如果你的程序使用了任何全局變量,編譯器可能會爆出一個警告信息,說變量是“假定為特殊變量(assumed to special)”。特殊變量將會在進階話題中解釋,你可以忽視警告信息,使用defvar重新聲明變量。
第二,如果你的程序包含了宏,宏定義必須是直接存在與文件中。如果函數foo調用一個宏bar,lisp可能無法認識到當編譯foo的時候需要將bar的調用看做一個宏調用來展開。如果foo已經被不正確的編譯了,發現bar是一個宏的時候,大部分編譯器將會報出警告。
第三,如果你的程序沖定義了任何內建函數,編譯器不會正確編譯他們。確保使用和內建函數不沖突的名字。在線文檔會幫助你來判斷。
14.11 案例學習:有限狀態機
有限狀態機(Finite state machine FSM)是一種來自理論計算機技術的,用來描述一種簡單的機器,比如售貨機或者交通指示燈工作狀態的技術。在這一節我們會寫一個有限狀態機模擬器來顯示真實的lisp程序是如何被開發出來的。為了討論的更加具體一些,我們會聚焦在一個特定的機器來進行模擬,但是我們的虛擬機將會為任何有限狀態機工作。
假設一種售貨機只出售兩種商品:口香糖和薄荷糖。口香糖要15美分,薄荷糖要20美分。任何5分和10分的組合可能被用來操作這個機器;他會自動改變組合。如果提供了足夠的錢,按下相應鍵會出現相應的商品,任何時候按下退款鍵就會返回剩下的錢。
這個機器一開始初始化成一個狀態叫做start,如果輸入的是字符nickel,他會打印“clunk”并且轉移到叫做have-5的狀態。如果實在have-5的狀態并且取得了dime作為輸入,就會打印“clink”,然后轉入狀態have-15。在have-15狀態如果輸入gum-button,就會輸出一包口香糖(gum),然后進入狀態end。
狀態機總共有六個狀態:start,have-5,have-10,have-15,和have-20,還有end。(之所以被稱作有限狀態機的原因就是他的狀態時有限的),每一個狀態都表現為一個節點,從一個狀態到另一個狀態之間的轉換都是一個箭頭。狀態都需要標記成一個一個轉換的機器動作,例如,從have-10到have-15的轉換就被標記為nickel。
在售貨機的全部定義中,defnode宏和defarc宏提供了簡單的語法來定義有限狀態機一部分。下面有限狀態機模擬器中你可以看到工作的模式和目標。
從定義節點和狀態的結構體開始,我們來構造我們虛擬機。,誒一個節點都有自己的名字,自己狀態輸入的列表,還有輸出狀態的列表。每一個狀態都有一個from節點和一個to節點,一個標記和一個動作。我們也定義了打印函數。
現在我們需要一個全局變量nodes來保存包含機器節點的列表,還有全局變量arc來保存狀態的列表。另一個變量,“current-node”,來保存機器狀態的記錄。用defvar來聲明這些變量,哦我忙呢會在進階話題里討論這個。函數initilize將這些變量設置成nil。
宏defnode是用來定義新節點的狀態包裹(syntactic sugar)。他在參數前面加上一個引號來調用addnode函數。
add-node構造一個新的節點,給這個節點一個名字,把這個節點加入到全局變量nodes中。因為它使用nconc(append的破壞性版本)所以可以再列表的最后加上一個節點。這確保了在nodes中的幾點會以defnode定義的順序出現。而不是相反的順序。add-node也會返回新建的節點。
FIND-NODE函數接受一個節點的名字作為輸入,然后返回一個對應的節點如果對應節點不存在,那么就會報錯 。
宏defarc提供了一個方便的定義狀態的語法,并且函數add-arc做一些實質上的工作。當一個狀態唄創建,他被加入node-ooutputs列表還有node-inputs列表。也不會被加入保存全局變量arcs的列表。
現在我們可以寫一個頂層函數fsm。他會接受一個可選的輸入來定義狀態機的初始狀態。默認的初始狀態是start。fsm反復調用函數one-transtation來轉移到下一個狀態。當狀態機到達一個沒有輸出的狀態(比如end),就會停止,請注意do是沒有一個空的變量列表的。
最后,我們來寫一個one-transition函數。他會提示輸入并且通過改變current-node的值來創建合適的狀態。如果從輸入無法給出一個合法的狀態的話,就會打印一個錯誤消息并且提示再一次輸入。
我們的模擬器不僅僅限制在模擬售貨機。任何可以被描述為有限的狀態,而且狀態可以互相轉換的設備都可以用這個程序模擬。
小結
宏是一種很有用處的lisp縮略法。它允許程序員來定義lisp的語法擴展,并且將功能實現的更加精煉。他也會幫助供應商向消費者隱藏大量細節的實現定義。宏不會對參數求值;他們返回會被lisp求值的表達式。新的宏可以被defmarco定義。
就像宏一樣,特殊函數不會對輸入求值。但是和宏不同的是,他們不會返回lisp表達式。特殊函數提供lisp創建的原始函數,比如賦值,條件是和塊結構。
反引號字符從模板中構造一個列表。如果模板元素的前面是一個逗號,他就會被求值。也就是會被插入進列表的值。前導逗號和at符號的元素會被連接在列表的后面。反引號在宏中式特別有用的,可以在模板中加入空格來構造特別復雜的表達式。
本章涉及函數
宏定義:DEFMACRO.
編譯器: COMPILE, COMPILE-FILE.
Lisp Toolkit: PPMX
ppmx是pretty print marco expansion的縮寫。他的宏展開第一個參數(不求值)并且打印其余的參數。ppmx不僅對學習內建宏,諸如setf十分有用,也會對調試自己寫的宏有幫助,如果在展開的時候有什么問題的話。
如果一個宏展開成另一個宏調用,ppmx會展示兩者的結果,第一個表達式和最后一個表達式推導的,當所喲的表達式都被展開的時候。例如,宏length-incf會展開成一個setf宏的調用。setf再展開成一個setq的特殊函數。
在一些實現中,dotimes宏展開成一個do宏的調用。在下面的例子中,do依次展開成一個更加復雜的表達式,包括block,let,tagbody和go。在本書中,我們不會討論tag的函數體和go。
第十四章進階話題
14.12 lambda列表關鍵字&BODY
人們寫宏的理由之一是他可以給lisp加上新的語法。例如,我們可以寫一個while宏來提供和在其他語言中一樣的控制結構。
while宏接受一個測試表達式作為第一個參數,如果測試為真,他之后的函數體表達式會被求值。函數體表達式會被關鍵字&rest手機,但是common lisp包含一個特殊的關鍵字,&body,用在剩下的參數是從一些控制結構中提取的宏。一些lisp編輯器將注意力放在關鍵字切分調用成宏的階段。&body關鍵字的使用也是意味著宏定義的讀者可已經剩下的參數作為lisp代碼的函數體。
函數NEXT-POWER-OF-TWO使用while循環來反復加倍變量I的值,從1開始,知道第一個兩倍數大于輸入的值n。
在最好的風格中,特定的額問題應該使用do而不是while來解決,這是要避免顯式的setf調用。
14.13 破壞lambda列表
宏MIX-AND-MATCH接受連個對偶(pair)作為輸入然后返回生成四對的表達式。
在上面的例子中,我們手動輸入了(FRED WILMA)和(BARNEY BETTY)。但是既然宏是不對自己的參數氣質的,他們就可以將輸入的表達式自動作為列表結構的一部分。這就是我們已知的破壞性。你可以定義如何破壞一個表達式,通過使用另一個整個參數列表替換宏參數列表的變量來實現。例如,我們可以用(x1 y1)替換MIX-AND-MATCH中的p ,還有(x2 y2)來替換。之后的版本就是使用破壞性的。
破壞性值在宏之中可以使用,在普通函數中不可以。對于宏來說,定義新的復雜語法的控制結構的視乎,破壞性就和有用了。宏dovector接下來就是模仿dotimes和dlist了。后續的步驟就是步進向量中的元素了。宏使用破滑行的手段來提取向量中的索引變量,向量表達式和結果形式。
你可以從dovector的展開中看出,為什么這個宏是一種很有用的縮略法。
dovector使用本地變量vec-dov(儲存向量)和len-dov(儲存長度),還有索引變量i-dov來展開成一個do表達式。選擇這些名字是因為他們不會與用戶的任何變量產生沖突。如果我們使用了vec,len和i,作為變量名,可以防止用戶訪問一些他們自己的變量。展開在do*的函數體中也會包含變量x的顯式賦值,更深層次的lisp展開是由block,lettagbody和go組成的,這個dovector表達式對于人類閱讀來說更加合適一些。
14.14 宏和常量作用域(lexically scoping)
我們回到對于faulty-incf的觀察,一個incf的實現嘗試,作為一個函數而不是一個宏。假設我們在傳給函數之前就引用變量,(FAULTY-INCF ’A)。faulty-incf需要做兩件事,必須找出變量當前的值,還有就是替換原來的值。
對于全局變量也是可以的,回憶一下鉆具變量在字符的值單元的命名方式。我們可以使用內建函數symbol-value來訪問值單元,我們用setf或者內建的set函數可以存儲值。
函數看上去工作是很正確的,但是也只是對全局變量有效。如果我們嘗試使用在本地變量上,就會失敗。simple-incf不論是本地變量或者全局變量都可以使用。
在test-simple中宏simple-incf被展開成一個時候求值的表達式,在test-simple的常量內容當中。所以本地變量turnp是一個常量,這是沒有問題的。
在faulty-incf求值回溯圖中我們可以看到bug。在faulty-incf的函數體重,只有本地變量var是可見的。粗實線包括的內容是顯示了上層的turnip的常量內容。沒有值被賦予全局變量trunip是不可訪問的。所以當symbol-value找到值單元的時候會有個未賦值錯誤。
14.15 宏的歷史意義
相比普通函數和特殊函數,宏的特性之一就是語法是可定義的。這回是的程序員可以對自己的語法擴展定義有清晰地認識。使用擴展的人說明他們是定義的而不是內建的使用者。相比之下,像pascal這種語言的話是不可以加一個新的語句類型的。只有新的過程可以加。擴展pascal語法的唯一方式是寫一個預處理器或者修改編譯器。兩個方法都是不可操作的。
common lisp的很多特性在早期的方言中都是有程序員以宏包的方式定義的。例如setf和destruct,還有with-open-file宏。甚至defmarco是原始的擴展。(雖然lisp在一開始已經有宏了,在defmarco出現前,還是使用一個和累贅的方法實現的。)
在許許多多的人的聰明才智和辛勤輔助下,lisp已經持續演化了超過三十年了。這個進化也包括了宏。除了擴展lisp,宏也可以被用在定義一種新的語言上。用于人工智能的很多高級語言都是建立在lisp的基礎上。本書中,我們用common lisp宏創造了一個特殊圖形語言實現。
14.16 動態作用域
縱觀本書,我們已經在所有變量上使用了語法作用域。語法作用與以為這對于函數foo就可以訪問變量x,foo的定義必須出現在x定義的范圍內。如果foo定義在頂層的額haunted,只能訪問全局變量(加上任何定義的本地變量),但是如果函數定義是一個lambda表達式的話,在另一個函數bar的函數體中出現的話,就可以訪問abr的本地變量還有自己的。函數定義在bar外面就不能訪問了。
可以選擇語法作用域的特定叫做動態作用域,在common lisp之前,動態作用域是lisp的常態配置。語法作用域現在只在兩種方言中存在scheme和T。
動態作用域變量也可以誒特殊函數調用,當一個變量名被聲明為特殊的時候,變量就不會是任何函數的本地變量,他的值可以再任何地方被訪問。相比之下,常量作用域變量是指在定義的函數體內才可以訪問的。聲明一個變量為特殊的方法是使用defvar宏。
我們來比較變量的兩種作用域的效果。我們生命bird是一個動態作用域。我們會使用fish作為一個常量作用域變量,所以不應該使用defvar來定義。每一個變量都會被賦值初始化,之后再寫一個函數來引用每一個變量的值。
現在我們來看一下另種作用域的規則區別,我們會寫一個佳作fish和birds的函數,首先我們看熟悉的,常量作用域的情況fish。
在test-lexical中,表達式fish指向本地變量fish。本地變量對ref-fish是不可見的。在ref-fish函數體中的字符fish仍然指向全局變量fish。在求值回溯圖中你可以看到ref-fish被實現包裹,顯示了上層語法環境是全局環境。既然reffish不會創造一個自己的本地變量fish,任何出現fish的地方都是指向全局變量。值是(salmon tuna)。
在動態作用域的情況下,使用birds,測試函數訊早前一個的定義,但是行為不同。區別在與defvar的效果是將birds定義為特殊的。
當我們進入test-dynamic的函數體中,一個新的動態變量birds就會被創建。從現在直到我們離開函數體,每一次使用birds就會指向這個變量,甚至是在其他函數的函數體中。全局變量birds在新的動態變量存在的情況下就是不可訪問的。當test-dynamic返回的時候,動態變量birds就會消失,還有同名的birds會再次綁定全局變量。
對于動態變量,沒有特殊的求值回溯標記來表示。你可以簡單記住一些已經被defvar定義的名字,一旦這樣這樣做,所有的變量birds都會指向動態作用域。值就是(eagle vulture)。
動態作用域的求值規則是,如果遇到了粗實線不是指向全局變量的作用域,而是直接傳遞,繼續尋找創建的變量名。如果只想愛你個了全局環境,意味著沒有函數現在有一個這個名字的額全局變量,我們只用全局的值。
術語動態綁定的意思就是指在ref-birds里的變量birds沒有永久綁定在一個變量上,fish在ref-fish中關聯在一個全局變量上。也就是說,名字和真是變量的列檢是動態的。當ref-birds在test-dynamic內部被調用,字符birds指向由test-dynamic建立的動態變量。當ref-birds在頂層被調用,同樣的字符birds是被解釋為全局變量的引用。
動態作用域應該謹慎使用,在早期的lisp方言中他是默認的,這引起了很多程序bug的出現,一個程序意外修改了一個另一個程序創建的動態變量。語法作用域保護一個函數的本地變量不會被其他無關函數修改。但是也有一些環境,動態作用是不二之選。
14.17 DEFVAR, DEFPARAMETER, DEFCONSTANT
DEFVAR, DEFPARAMETER, 和DEFCONSTANT都是聲明名字是特殊的函數。defvar被用來聲明變量的值會在程序的正常操作中改變,他會接受一個可循啊的初始值,和一個文檔字符串。
一個有意思的事實是defvar是不會改變已經由值的變量的。只會賦值那些沒有值的變量。
DEFPARAMETER和DEFVAR的語法相同,但是會被用在聲明值不會被程序運行時改變的變量上。他們有參數設定的能力,也就是告訴程序如何行為。另一個區別就是。DEFPARAMETER會給已經有值的變量賦值。
備用咋定義常量,也就是值絕對不會被改變。在Lisp中的慣例是使用星號包裹變量名,但是這個而不是用在常量上的。嘗試改變一個常量的值就會報錯,或者常在一個相同名字的新的變量作為常量。PI就是一個內建的常量。
如果是一個變量的話,聲明一個變量為常量有時候允許編譯器生成更加有效率的機器語言,也會防止意外的改變變量的值。大部分實現仍然允許你故意改變值,雖然是在調試器當中。
14.18 重綁定特殊變量
很多動態作用域時代的lisp變量術語對今天來說是過時的,由于歷史原因,一些作者在談論創建變量的時候是說綁定一個變量的。也說:“未綁定變量”,在說”未賦值變量“的時候。綁定不是一定指賦值,這是lisp術語系統中的一大迷惑的地方。非全局變量總是有值的,但是全局變量和特殊變量也有可能沒有值。在本書中,我們不會談論很晦澀的細節。
到現在為止我們已經避免了因為使用綁定帶來的困擾。在最后一節中我們會介紹術語重綁定來指向使用舊的名字來創造一個全新的變量。新的變量的存在的那個時候,在程序的任何地方這個名字都會指向這個變量。之前的那個變量就會不能使用。嚴格來說,我們不會重綁定任何變量,我們動態綁定名字,暫時指向不同的變量。
common lisp包含了很多內建的特殊變量,一些事輸入輸出的時候使用的。例如,變量print-base就是format函數和其他函數決定那個數值來打印的變量。既然已經定義為特殊,我們就不是一定要使用defvar。為了重綁定他,我們僅僅包括在我們函數的參數列表中。
我們也可以使用let來重綁定變量。當一個特殊變量被重綁定,人格賦值,無論在程序的何處,將會影響新的變量而不是舊的那個。在接下來的例子中,當bump-foo在let的函數體中被調用,他的增值的是let中建立的動態變量foo。當他在let外部被調用,他增值的就是全局變量foo。如果foo還沒有被聲明為特殊,bump-over總是可以訪問全局變量foo。
當大型程序的不同部分需要互相交換信息的時候,特殊變量的重綁定是最有用的,并且想要通過附加參數給喊出傳遞變量是不可行的。寫一個真實的大型程序需要很多技能,比這本書所說的多得多,這是一個進階lisp課程的很棒的主題。
進階話題涉及函數
DEFMACRO: lambda列表關鍵字&BODY
聲明: DEFVAR, DEFPARAMETER, DEFCONSTANT