第三章 EVAL標(biāo)記法
3.1 導(dǎo)引
在進(jìn)一步深入學(xué)習(xí)Lisp之前,我們必須切換到一個(gè)更加適合的標(biāo)記法,EVAL標(biāo)記法。不再使用矩形來表示函數(shù),而是使用列表。矩形圖示是很容易理解,但是EVAL表示法有以下幾個(gè)優(yōu)點(diǎn):
- 一些復(fù)雜到不能用矩形圖示來表示的編程概念,可以用EVAL表示來表達(dá)。
- EVAL圖示是很方便就可以夠用鍵盤敲擊來輸入的,盒形圖示就不行。
- 從數(shù)學(xué)的角度來看,使用一般列表來表示函數(shù)是非常優(yōu)雅的,因?yàn)槲覀兛梢源_定使用同一種表達(dá)式來表示函數(shù)和數(shù)據(jù)。
- 在Lisp中,數(shù)據(jù)就是函數(shù),EVAL表示可以允許我們定義函數(shù)來接受其它函數(shù)作為輸入。我們將在第7章介紹這個(gè)特性。
- 當(dāng)你精通了EVAL表達(dá)式,那也就是掌握了用Lisp和電腦交流的方式。
3.2 EVAL函數(shù)
EVAL函數(shù)是Lisp的核心。主要的作用就是求Lisp表達(dá)式的數(shù)值并且計(jì)算結(jié)果。大部分表達(dá)式是由一個(gè)函數(shù)和一些輸入組成的,假如給EVAL函數(shù)輸入(+ 2 3),他就會(huì)調(diào)用內(nèi)建函數(shù)+來處理輸入2和3.并且+函數(shù)返回?cái)?shù)值5,因此我們會(huì)說,表達(dá)式(+ 2 3)的值是5。
從現(xiàn)在開始,我們就不再畫一個(gè)EVAL函數(shù)的矩形了,而是使用一個(gè)箭頭來表示。
(+ 2 3) → 5
如果使用兩個(gè)箭頭的話,會(huì)顯得稍稍冗長(zhǎng)一些。
想要展現(xiàn)更多細(xì)節(jié)的話,就會(huì)使用三箭頭的圖形表示。
細(xì)線和粗線代表的意思等一會(huì)兒再解釋,我們先來看一些EVAL表達(dá)式的例子。
(+ 1 6) → 7
(oddp (+ 1 6)) → t
(* 3 (+ 1 6)) → 21
(/ (* 2 11) (+ 1 6)) → 22/7
3.3 EVAL表達(dá)式可以做到矩形表達(dá)式所做的任何事
很明顯,矩形表達(dá)式能夠表示的表達(dá)式,那么EVAL表達(dá)式也能夠做到。
比如下面的表達(dá)式就可以寫成
類似的EVAL表達(dá)式也可以寫成矩形表達(dá)
(* 3 (+ 5 6))
(not (equal 5 6))
你可能注意到了,矩形表達(dá)式和EVAL表達(dá)式讀起來正好是相反的順序,矩形表達(dá)式的順序應(yīng)該是,5,6,EQUAL,NOT。相對(duì)應(yīng)的EVAL表達(dá)式的讀法是NOT,EQUAL,5,6.在矩形表達(dá)式中,計(jì)算式從左向右的計(jì)算順序。在EVAL表達(dá)式中一個(gè)函數(shù)的輸入時(shí)從左向右這么被處理的。但是對(duì)于嵌套結(jié)構(gòu)的表達(dá)式來說,求數(shù)值的真正過程是從最深處的表達(dá)式開始從內(nèi)向外的計(jì)算順序,使得函數(shù)調(diào)用的順序是從右向左的。
3.4 求值規(guī)則定義EVAL函數(shù)的行為
EVAL函數(shù)的運(yùn)行是伴隨一系列求值規(guī)則的。其中一個(gè)規(guī)則就是數(shù)字和其他確定的對(duì)象是自求值的,意思就是自己求自己的值。特殊字符串T和NIL也是自求值的。
23 → 23
t → t
nil → nil
數(shù)字,T,NIL的求值規(guī)則:數(shù)字,字符串T,NIL是自求值的。
也有規(guī)則是針對(duì)列表的。列表的第一個(gè)元素將會(huì)定義一個(gè)待調(diào)用的函數(shù);剩余的元素是函數(shù)的非求值參數(shù)。這些元素必須是被求值,從左到右的順序來決定函數(shù)的輸入。例如,求值表達(dá)式(ODDP (+ 1 6)),首先要做的就是求值ODDP函數(shù)的參數(shù),列表(+ 1 6)。為了實(shí)現(xiàn)求值這個(gè)參數(shù),我們開始求值函數(shù)+的參數(shù)。1求值得1,6求值得6,?,F(xiàn)在我們可以調(diào)用函數(shù)+使用這些輸入得出結(jié)果7,7就是作為函數(shù)ODDP的輸入,然后返回結(jié)果T。
列表的求值規(guī)則:列表的第一個(gè)元素就定義了一個(gè)待調(diào)用的函數(shù),余下的元素就是這個(gè)函數(shù)的參數(shù)。調(diào)用函數(shù)操作這些已求值的參數(shù)。
以下圖示是稱作求值回溯圖(evaltrace diagram),展示了列表(ODDP (+ 1 6))是如何求值的。請(qǐng)注意求職過程是從內(nèi)部嵌套的表達(dá)式(+ 1 6)開始,到外層表達(dá)式,ODDP函數(shù)結(jié)束。這種由內(nèi)而外的性質(zhì)是由求值回溯圖的形狀來反映。
這里有另一個(gè)在函數(shù)調(diào)用之前,參數(shù)被求值的例子,表達(dá)式(EQUAL (+ 7 5) (* 2 8))的求值回溯圖。
練習(xí)題
3.1 列表(NOT (EQUAL 3 (ABS -3)))的求值結(jié)果是?
3.2 用EVAL表達(dá)式來寫一個(gè)表達(dá)式,8加上12再除以2.
3.3 一個(gè)數(shù)的平方可以讓這個(gè)數(shù)乘以自己來求得,寫一個(gè)EVAL表達(dá)式來求3的平方加上4的平方。
3.4 畫出下列EVAL表達(dá)式的求值回溯圖。
(- 8 2)
(not (oddp 4))
(> (* 2 5) 9)
(not (equal 5 (+ 1 4)))
3.5 用EVAL表達(dá)式定義函數(shù)
在矩形表達(dá)式中,我們通過表現(xiàn)內(nèi)部的具體細(xì)節(jié)來定義一個(gè)函數(shù)。函數(shù)的輸入被描繪成為箭頭,在EVAL表達(dá)式中,我們使用列表來定義函數(shù),通過變量名字來指向函數(shù)的參數(shù)。我們可以命名矩形表達(dá)式的輸入通過在箭頭上寫上名字的方式。
以EVAL表達(dá)式來定義AVERAGE函數(shù)
(defun average (x y)
(/ (+ x y) 2.0))
DEFUN函數(shù)是一類特殊的函數(shù),名字叫做宏函數(shù)(macro function),規(guī)定他的參數(shù)并不是要求值的,所以宏函數(shù)不是一定要背引用的。DEFUN函數(shù)的作用是用來定義其它函數(shù)。第二個(gè)元素是參數(shù)列表,也就是定義將會(huì)用到的參數(shù)的名字。剩下的元素就是DEFUN函數(shù)定義的函數(shù)體(body),函數(shù)的內(nèi)部細(xì)節(jié)。另外DEFUN這個(gè)名字是define的縮寫。
只要在計(jì)算機(jī)中定義函數(shù)AVERAGE一次,就可以使用EVAL表達(dá)式來調(diào)用AVERAGE函數(shù)。比如這樣調(diào)用,(AVERAGE 6 8),AVERAGE函數(shù)就會(huì)使用6作為X的值,8作為Y的值來進(jìn)行計(jì)算,最后得出結(jié)果7。
這里還有一個(gè)defun定義函數(shù)的例子
(defun square (n) (* n n))
函數(shù)名叫做square,參數(shù)列表是(n),也就是接受一個(gè)指向n的參數(shù)。函數(shù)提示表達(dá)式(* n n),理解這個(gè)定義的正確方式是定義一個(gè)n的square的函數(shù),返回n的平方。
除了特殊字符串T和NIL之外,幾乎所有的字符串都可以作為函數(shù)的參數(shù)名使用,x,y,n一般是最常用的,但是bozo,artichoke等等也是可以的。一個(gè)計(jì)算貿(mào)易成本的函數(shù)可能給函數(shù)參數(shù)的命名就是quantity,price,和handling-charge。
(defun total-cost (quantity price handling-charge)
(+ (* quantity price) handling-charge))
3.6 變量(variables)
變量(variable)是數(shù)據(jù)存儲(chǔ)的地方。我們?cè)賮砜碼verage函數(shù)。當(dāng)我們調(diào)用average函數(shù),Lisp創(chuàng)造了兩個(gè)變量來存儲(chǔ)輸入,之后函數(shù)體中的表達(dá)式會(huì)根據(jù)參數(shù)名來指向這個(gè)變量。這些變量的名字就是x和y。很重要的一點(diǎn)就是辨析清楚變量和字符串之間的區(qū)別。變量不是字符串,變量是通過字符串來命名的,函數(shù)也是通過字符串命名。
變量的值就是所存儲(chǔ)的數(shù)據(jù)。當(dāng)我們對(duì)列表(average 3 7)求值的時(shí)候,Lisp創(chuàng)造出變量x和y,并且給他們分別賦值,3和7.在average函數(shù)的函數(shù)體中,字符串x指向第一個(gè)變量,字符串y指向第二個(gè)變量。這些變量也只能在函數(shù)體內(nèi)部被引用,在average函數(shù)外部就不能引用了。當(dāng)然字符串x和y在函數(shù)外部仍然存在,但是與在函數(shù)內(nèi)部的意思已經(jīng)完全不同,下面的求值回溯圖很好地說明了計(jì)算結(jié)果的過程:
術(shù)語“變量“的使用是計(jì)算機(jī)編程中特有的,在數(shù)學(xué)里,一個(gè)變量的意思是一個(gè)未知數(shù)據(jù)的意思,不是一個(gè)計(jì)算機(jī)內(nèi)存中的數(shù)據(jù)。但是這兩個(gè)方面的意思也并不是完全對(duì)立,一個(gè)函數(shù)的輸入在函數(shù)定義的時(shí)候也的確是未知的一個(gè)數(shù)據(jù)。
現(xiàn)在,我們可以解釋求值回溯圖中粗箭頭和細(xì)箭頭的意思了。細(xì)箭頭是用表達(dá)式的值連接起來的。一個(gè)粗箭頭是被用在戰(zhàn)士進(jìn)入以一個(gè)函數(shù)內(nèi)部的函數(shù)體和從函數(shù)體中出來,得到結(jié)果的過程。在粗箭頭包括的范圍內(nèi),我們見到的是函數(shù)內(nèi)部的運(yùn)行情況。咋average函數(shù)的函數(shù)體中,變量被創(chuàng)造,表達(dá)式被求值。對(duì)于像+或者/這類函數(shù),因?yàn)樗麄兪窃己瘮?shù),所以我們沒什么機(jī)會(huì)用粗箭頭來看他們內(nèi)部的情況。對(duì)于用于定義的函數(shù)average,我們可以由一個(gè)細(xì)箭頭開始表示表達(dá)式函數(shù)調(diào)用,然后中間表示粗箭頭來展現(xiàn)函數(shù)體。下面用抽象語法來展現(xiàn)這種表達(dá)方式:
求值回溯表達(dá)式是十分靈活的:如果需要可以隱去不必要的細(xì)節(jié),比如不展示函數(shù)體。另一個(gè)簡(jiǎn)化求職回溯過程的方法是不展示數(shù)字的求值,既然數(shù)字都是自求值的。有時(shí)候也省略字符串的求值。下圖是ONEMOREP函數(shù)的一般簡(jiǎn)要格式的回溯圖:
3.7 對(duì)字符串求值
一個(gè)函數(shù)給自己參數(shù)使用的名字和其它函數(shù)使用的參數(shù)名字是互相獨(dú)立的。兩個(gè)函數(shù),比如half和square函數(shù)都可以叫他們的參數(shù)n,但是在函數(shù)half中的n只是指向half函數(shù)的輸入,與square函數(shù)中的n沒有任何關(guān)系。
EVAL表示法中對(duì)于字符串的規(guī)則是很簡(jiǎn)單的:對(duì)于字符串的求值規(guī)則:一個(gè)字符串的值就是變量指向的數(shù)據(jù)。
在函數(shù)half和square的函數(shù)體之外,字符處啊n指向的就是全局變量(global variable)n。全局變量就是和任何函數(shù)都沒有關(guān)系的變量。PI作為Common Lisp的內(nèi)建變量就是一個(gè)全局變量的例子。
pi → 3.14159
Lisp程序員有時(shí)候非正式討論變量求值的時(shí)候,他們會(huì)說變量求他們的值,真正的意思是一個(gè)字符串求他所指向的變量的值,既然有許許多多變量都被稱作n,那么n指向甚么值就看變量n在什么地方出現(xiàn)了。假如出現(xiàn)是在square函數(shù)的函數(shù)體內(nèi)部,那么得到的變量就是函數(shù)square的輸入,假如出現(xiàn)在函數(shù)體外部,那么得到的就是全局變量n。
假如你求得變量還沒有被賦值,Lisp就會(huì)報(bào)錯(cuò),變量未賦值錯(cuò)誤(unassigned variable error)。比如,在內(nèi)建變量中,并沒有一個(gè)佳作eggplant的變量。那對(duì)字符串eggplant的求值就會(huì)報(bào)變量未賦值錯(cuò)誤。當(dāng)然,除非在某函數(shù)的內(nèi)部對(duì)這個(gè)字符串求值,指向某一個(gè)輸入。
eggplant → Error! EGGPLANT unassigned variable.
Common Lisp中也沒有叫做n的內(nèi)建變量,所以在函數(shù)half和square的函數(shù)體外部求n的值也是會(huì)報(bào)同樣的錯(cuò)誤。
3.8 將字符串和列表作數(shù)據(jù)使用
假設(shè)我們想要調(diào)用函數(shù)equal,并且將字符串kirk和spock作為輸入。在矩形表達(dá)式中這是很方便的,因?yàn)樽址土斜砜偸潜划?dāng)做數(shù)據(jù)看待。但是在EVAL表達(dá)式中,字符串是被看做具名變量的,所以假如我們寫成這樣。
(equal kirk spock)
Lisp將會(huì)認(rèn)為我們嘗試比較的是兩個(gè)名字叫kirk和spock的全局變量的值。既然給不出這樣兩個(gè)變量的值,那就會(huì)報(bào)錯(cuò)。
(equal kirk spock) → Error! KIRK unassigned variable.
我們真正想要比較的不是變量的值,而是量給字符串本身,我們告訴Lisp,這個(gè)兩個(gè)字符串不是變量的引用而是作為數(shù)據(jù)看待,只要在字符串前面加上一個(gè)單引號(hào)就好了。
(equal ’kirk ’spock) → nil
因?yàn)樽址甌和NIL都是被求值為自己,所以不需要用單引號(hào)來標(biāo)記他們,其他字符串需要這么標(biāo)記。
(list ’james t ’kirk) → (james t kirk)
一個(gè)字符串是否在函數(shù)定義中被看做數(shù)據(jù),或者在函數(shù)輸入中被略過,為了防止被求值,就必須加上前面的單引號(hào)。
(defun riddle (x y)
(list ’why ’is ’a x ’like ’a y))(riddle ’raven ’writing-desk) → (why is a raven like a writing-desk)
列表在被當(dāng)做數(shù)據(jù)看的時(shí)候也必須加引號(hào),不然就會(huì)被Lisp求值,一個(gè)典型的結(jié)果就是報(bào)錯(cuò),未定義函數(shù)錯(cuò)誤(undefined function)。
(first (we hold these truths)) → Error! WE undefined function.
(first ’(we hold these truths)) → we
對(duì)引用對(duì)象的求值規(guī)則:一個(gè)被引用的對(duì)象求值為自己本身。
這里有一些加引號(hào)和沒加引號(hào)的列表,之間的區(qū)別很明顯
(third (my aunt mary)) → Error! MY undefined function.
(third ’(my aunt mary)) → mary
(+ 1 2) → 3
’(+ 1 2) → (+ 1 2)
(oddp (+ 1 2)) → t
(oddp ’(+ 1 2)) → Error! Wrong type input to ODDP.
最后一個(gè)例子出錯(cuò)的原因是oddp被調(diào)用的時(shí)候,列表(+ 1 2)的值需要作為輸入。引號(hào)的出現(xiàn)使得列表的求值被繞過,oddp接收到的是列表而不是數(shù)字,oddp的輸入必須是數(shù)字,不能是列表。
現(xiàn)在我們來看一下有引號(hào)存在的求值回溯過程
3.9 錯(cuò)誤引用問題
對(duì)于Lisp程序員來說,很容易就對(duì)引號(hào)感到困惑,而且引號(hào)的使用有時(shí)候不是放錯(cuò)地方就是在需要的地方少用了引號(hào)。Lisp提供的錯(cuò)誤信息有時(shí)候是很好的關(guān)于錯(cuò)誤的提示。一個(gè)未賦值變量錯(cuò)誤或者一個(gè)未定義函數(shù)錯(cuò)誤通常都顯示是一個(gè)引號(hào)被遺漏的原因。
(list ’a ’b c) → Error! C unassigned variable.
(list ’a ’b ’c) → (a b c)
(cons ’a (b c)) → Error! B undefined function.
(cons ’a ’(b c)) → (a b c)
另一方面來說,輸入類型錯(cuò)誤或者意想不到的輸出結(jié)果也可能是一個(gè)引號(hào)放錯(cuò)了位置的表示。
(+ 10 ’(- 5 2)) → Error! Wrong type input to +.
(+ 10 (- 5 2)) → 13
(list ’buy ’(* 27 34) ’bagels) → (buy (* 27 34) bagels)
(list ’buy (* 27 34) ’bagels) → (buy 918 bagels)
給一個(gè)列表加上引號(hào),放在列表外面的目的是為了防止列表被求值。如果引號(hào)被放在列表里面的話,EVAL表達(dá)式就會(huì)嘗試對(duì)列表進(jìn)行求值,結(jié)果就是錯(cuò)誤的。
(’foo ’bar ’baz) → Error! ’FOO undefined function.
’(foo bar baz) → (foo bar baz)
3.10 構(gòu)造列表的三種方式
有三種方式來通過EVAL表達(dá)式來構(gòu)造列表??梢园闪斜碇苯訉懗鰜?,然后前加引號(hào)防止被求值
’(foo bar baz) → (foo bar baz)
或者我們可以使用list和cons函數(shù)來為獨(dú)立元素構(gòu)造一個(gè)列表,假如我們使用這個(gè)方法,我們需要把每一個(gè)元素都前加引號(hào)。
(list ’foo ’bar ’baz) → (foo bar baz)
(cons ’foo ’(bar baz)) → (foo bar baz)
使用這種方法的一個(gè)好處就是,在構(gòu)成列表的元素匯總,可以有一些是被當(dāng)時(shí)計(jì)算出來的而不是直接定義的。
(list 33 ’squared ’is (* 33 33)) → (33 squared is 1089)
如果整個(gè)列表是被直接引用的話,那么列表內(nèi)部的所有元素都不會(huì)被求值。
’(33 squared is (* 33 33)) → (33 squared is (* 33 33))
我們也已經(jīng)見過一些引號(hào)沒有被正確使用的情況
(list foo bar baz) → Error! FOO unassigned variable.
(foo bar baz) → Error! FOO undefined function.
(’foo ’bar ’baz) → Error! ’FOO undefined function.
3.11 錯(cuò)誤定義一個(gè)函數(shù)的四種方式
對(duì)于初學(xué)者來說,用EVAL表達(dá)式來定義函數(shù),在正確處理語句結(jié)構(gòu)上會(huì)有一些問題。讓我們來看看這個(gè)問題,以函數(shù)intro的定義為例:
(defun intro (x y) (list x ’this ’is y))
(intro ’stanley ’livingstone) → (stanley this is livingstone)
請(qǐng)注意,intro函數(shù)的參數(shù)列表是由兩個(gè)字符串組成,x和y,既沒有引號(hào)也沒有圓括號(hào),在函數(shù)體中的變量x,y也是,沒有引號(hào)和圓括號(hào)。
第一種錯(cuò)誤定義函數(shù)的方式就是,參數(shù)列表中的字符串被加上了其他東西。如果參數(shù)列表中的參數(shù)被加上了引號(hào)或者括號(hào),那么函數(shù)就不會(huì)正常工作。初學(xué)者常常嘗試去給參數(shù)列表加上一些東西,想要讓列表還取代字符串作為輸入,這是第一個(gè)錯(cuò)誤。
(defun intro (’x ’y) :bad argument list
(list x ’this ’is y))
(defun intro ((x) (y)) :bad argument list
(list x ’this ’is y))
第二種錯(cuò)誤定義的方式是,在本不該出現(xiàn)括號(hào)的函數(shù)體里出現(xiàn)了多余的括號(hào)只有函數(shù)調(diào)用會(huì)有括號(hào)包圍,在變量上包圍括號(hào)將會(huì)引發(fā)函數(shù)定義錯(cuò)誤。
(defun intro (x y) (list (x) ’this ’is (y)))
(intro ’stanley ’livingstone) → Error! X undefined function.
第三種錯(cuò)誤定義的方式是給一個(gè)變量加上引號(hào)。字符串必須沒有引號(hào)才會(huì)被認(rèn)為是指向變量的。這里有一些例子來指出給變量加上引號(hào)會(huì)有什么事情發(fā)生。
(defun intro (x y) (list ’x ’this ’is ’y))
(intro ’stanley ’livingstone) → (x this is y)
The fourth way to misdefine a function is to not quote something that
should be quoted. In the INTRO function, the symbols X and Y are variables
but THIS and IS are not. If we don’t quote THIS and IS, an unassigned
variable error results.
第四種錯(cuò)誤定義的方式是沒有給應(yīng)該加上引號(hào)的地方加上引號(hào)。在intro函數(shù)中,字符串x和y都是變量,但是this和is不是變量。加入我們沒有給this和is加上引號(hào),一個(gè)未賦值錯(cuò)誤就會(huì)出現(xiàn)。
(defun intro (x y) (list x this is y))
(intro ’stanley ’livingstone) → Error! THIS unassigned variable.
3.12 關(guān)于變量的更多
在Lisp中,一個(gè)函數(shù)當(dāng)被調(diào)用的時(shí)候會(huì)自動(dòng)創(chuàng)建變量,一般來說,函數(shù)運(yùn)行結(jié)束的時(shí)候會(huì)把變量銷毀。來看一下double函數(shù),每次被調(diào)用的時(shí)候都會(huì)創(chuàng)造一個(gè)叫做n的變量:
(defun double (n) (* n 2))
在double函數(shù)的外部。字符串n指向全局變量n,全局變量n沒有被賦值,所以對(duì)nde求值就會(huì)發(fā)生錯(cuò)誤。
n → Error! N unassigned variable.
假設(shè)我們對(duì)(double 3)求值,在double函數(shù)內(nèi)部,字符串n指向一個(gè)新創(chuàng)建的變量,會(huì)綁定在輸入上,并不是綁定全局變量n上下面的求值回溯圖展現(xiàn)了這一過程。
如果我們?cè)僖淮握{(diào)用double函數(shù),例如(double 8),一個(gè)全新的變量n就會(huì)被創(chuàng)造出來并賦值8.在double函數(shù)外部,n指向全局變量n,沒有被賦值的變量?,F(xiàn)在我們來嘗試舉例有兩個(gè)變量的例子,這里是函數(shù)quadruple,通過double函數(shù)定義。
(defun quadruple (n) (double (double n)))
double函數(shù)和quadruple函數(shù)都會(huì)調(diào)用輸入n,假設(shè)我們對(duì)表達(dá)式(quadruple 5)求值,如下面的圖像表示。當(dāng)我們進(jìn)入函數(shù)quadruple背部,Lisp創(chuàng)造一個(gè)全新的變量n并且賦值5然后求值表達(dá)式(double (double 5))。當(dāng)函數(shù)double的輸入5的時(shí)候會(huì)發(fā)生什么?double函數(shù)創(chuàng)建了自己的變量n,綁定自己的輸入5。double函數(shù)的函數(shù)體求值10,現(xiàn)在我們已經(jīng)對(duì)(double n)求值了,鎖我我們可以用這個(gè)結(jié)果來對(duì)(double (double n))求值,double函數(shù)被再一次調(diào)用,這一次的輸入是10,所以另一個(gè)變量n被創(chuàng)造出來,并且綁定輸入,10.然后求值(* n 2)表達(dá)式,之后double函數(shù)返回20,quadruple函數(shù)返回29作為結(jié)果,然后我們就就是并再一次回到了頂層,變量n指向全局變量的地方。
小結(jié)
在本章我們學(xué)習(xí)了EVAL表達(dá)式,使用將函數(shù)等用列表形式來表現(xiàn)的方法。Lisp根據(jù)一系列的求值規(guī)則來用EVAL表示法表示函數(shù),有關(guān)求值規(guī)則,我們學(xué)習(xí)了:
- 數(shù)字是自求值的,求值也就等于自身,T和NIL也是如此。
- 在求值一個(gè)列表的時(shí)候,第一個(gè)元素被定義為一個(gè)函數(shù)來被調(diào)用,剩下的元素被定義為函數(shù)的參數(shù),這些參數(shù)從左到右來求值,將輸入導(dǎo)入傳遞給函數(shù)進(jìn)行操作。
- 字符串在如果不是一個(gè)列表的第一個(gè)元素,那么就會(huì)被解釋為指向變量。一個(gè)字符串將會(huì)求值為所命名的變量。準(zhǔn)確的說,一個(gè)變量的值取決于字符串出現(xiàn)的位置。變量如果沒有被賦值的話,一個(gè)未賦值錯(cuò)誤就會(huì)出現(xiàn)。
- 一個(gè)加上引號(hào)的列表或者字符串會(huì)被求值為自身。
用列表的格式(defun 函數(shù)名 (參數(shù)列表) 函數(shù)體)來定義一個(gè)函數(shù)。defun函數(shù)是一個(gè)特殊的函數(shù),他的輸入將不會(huì)被引用,一個(gè)函數(shù)的參數(shù)列表就是參數(shù)字符串被賦予的輸入值。在函數(shù)內(nèi)部,變量所具有的值指向函數(shù)的輸入。
本章所學(xué)的函數(shù)
求值器:EVAL.
定義函數(shù)的宏函數(shù): DEFUN.
在電腦上運(yùn)行Lisp
恭喜!你已經(jīng)完成了所有紙筆上的學(xué)習(xí)過程,現(xiàn)在是學(xué)習(xí)如何在計(jì)算機(jī)上使用Lisp的時(shí)候了。很不幸的是,我不能給你更加詳細(xì)的介紹l;有很多類型的計(jì)算機(jī)和更加多的Lisp實(shí)現(xiàn)。你可能想要多花一點(diǎn)時(shí)間來熟悉各種用戶手冊(cè),一個(gè)更加有效率的方法是找一個(gè)熟悉你所有的機(jī)器的人來請(qǐng)教。
3.13 運(yùn)行Lisp
The first thing you need to find out is how to start up Lisp on your computer.
If you’re lucky you can just type "lisp" and hit the Return key, but you might
have to type something more complicated. When Lisp starts up it prints a
greeting message. Each implementation has its own style of greeting, but a
typical message looks something like this:
首先需要明確的事情是在你的計(jì)算機(jī)上如何運(yùn)行Lisp。最好就是鍵入Lisp然后回車就好了,但是可能還需要更多更復(fù)雜的操作,當(dāng)Lisp開始運(yùn)行的時(shí)候或打印歡迎信息,每一個(gè)實(shí)現(xiàn)的歡迎信息都不相同,一半看起來是這樣。
CMU Common Lisp M2.8 (29-Mar-89)
Hemlock M3.0 (29-Mar-89), Compiler M1.7 (29-Mar-89)
Send bug reports and questions to Gripe.
這個(gè)出現(xiàn)在歡迎信息里的>符號(hào)叫做頂層提示符(top-level prompt)。他表示Lisp現(xiàn)在正在等待你鍵入一些東西。一些Lisp實(shí)現(xiàn)使用了不同的額提示符,許多是用的星號(hào)*.
接下來你需要確定的事情是一些Lisp的控制字符:
- 如何刪除一個(gè)字符:按下delete鍵,backspace鍵還是其他?
- 如何撤銷一整行的輸入,如果你不想要的話?在一些Lisp中你可以通過鍵入Control-U來實(shí)現(xiàn)這個(gè)功能(也就是按住ctrl鍵的同時(shí)按u)。
- 鍵入什么可以使得你回到頂層提示符?許多Lisp使用Control-G或者Control-C來實(shí)現(xiàn)。
當(dāng)哦我們面對(duì)一些特殊的字符的時(shí)候,請(qǐng)注意,計(jì)算機(jī)總是將不同的想相似字符分的很清楚的,比如字母o和數(shù)字0,啊還有字母l和數(shù)字1。當(dāng)對(duì)于閱讀的時(shí)候這些相似的字符對(duì)閱讀不會(huì)有什么障礙,但是輸入計(jì)算機(jī)的話就一定要清楚。
最后,你需要了解如何退出Lisp。大部分Lisp實(shí)現(xiàn)要求箭如意想(quit)或者(exit)來離開程序。一些時(shí)候文件結(jié)束字符想control-d也會(huì)起作用。
3.14 read-eval-print循環(huán)
一臺(tái)運(yùn)行Lisp的計(jì)算機(jī)很像一臺(tái)便攜式計(jì)算器,讀入一個(gè)鍵入的表達(dá)式,計(jì)算他(使用EVAL函數(shù)),然后在屏幕上打印結(jié)果,隨后另一個(gè)頂級(jí)提示符將會(huì)出現(xiàn),等待下一個(gè)表達(dá)式的輸入。這個(gè)過程被稱作read-eval-print loop。
這里有一個(gè)計(jì)算機(jī)和我們的鍵入函數(shù)之間的簡(jiǎn)單對(duì)話。在這個(gè)例子中,鍵入的信息會(huì)在提示符>后面以小寫出現(xiàn);計(jì)算機(jī)的返回會(huì)用大寫返回。不是所有Lisp實(shí)現(xiàn)都遵循這個(gè)慣例,但是大部分是的。
3.15 從錯(cuò)誤中恢復(fù)
一件很重要的事情就是學(xué)會(huì)如何從錯(cuò)誤中恢復(fù)。首先來考慮打字拼寫錯(cuò)誤,如果見入了很長(zhǎng)的一段文字記過發(fā)現(xiàn)在開始的地方有錯(cuò)誤,就會(huì)想要放棄整個(gè)一行表達(dá)式重新開始。Lisp中是現(xiàn)代額方法是control-G的輸入,然后回到頂級(jí)提示符,這里是一個(gè)例子:
一個(gè)更加普遍的問題是,表達(dá)式的拼寫正確但是結(jié)果是一個(gè)計(jì)算錯(cuò)誤。想要增加一個(gè)數(shù)字或者字符串是一半的做法。一個(gè)計(jì)算錯(cuò)誤出現(xiàn)的時(shí)候,Lisp會(huì)打印錯(cuò)誤信息,然后進(jìn)入一個(gè)不同的循環(huán)。不同于上文提到的read-eval-print循環(huán),我們現(xiàn)在稱他為調(diào)試器(debugger)的read-eval-print 循環(huán)。我們將在第八章學(xué)習(xí)如何使用調(diào)試器,到現(xiàn)在為止,你需要知道的是如何才能離開調(diào)試器回到頂級(jí)提示符。在Lisp中,control-G就是這個(gè)脫出命令。
如果你已經(jīng)在Lisp中定義了一個(gè)函數(shù)但是卻不能正常工作的話,那就重新定義一下然后再次嘗試。你可以重新定義很多次,沒有限制,只有最后一次定義會(huì)被保存和使用。接下來的例子就顯示這個(gè)特性,也顯示了你可以在一個(gè)表達(dá)式輸入的時(shí)候,在任意地方回車都沒有關(guān)系。這是因?yàn)楸磉_(dá)式是列表??崭窈涂s進(jìn)不會(huì)有影響。
請(qǐng)確認(rèn)你沒有使用想cons,+,或者list這類內(nèi)建函數(shù)的名字,重新定義這些函數(shù)會(huì)出現(xiàn)比較嚴(yán)重的錯(cuò)誤,你可能需要離開里搜譜然后重新開始,而且之前定義的任何函數(shù)將會(huì)被廢棄。
Lisp Tookit:ED
出現(xiàn)在這里L(fēng)isp Toolkit小節(jié)和后續(xù)的章節(jié)會(huì)介紹Lisp編程環(huán)境的一些重要工具。在這些工具中,比如語言定義編輯器(Language-specific editor),編程格式轉(zhuǎn)換(program formatters),和代碼層次的調(diào)試器(source-level debugger)都可以在其他編程語言中找到并且使用。但是他們是最先出現(xiàn)在Lisp中的。其他工具仍然是Lisp獨(dú)有的,其中兩個(gè),sdraw和dtrace,是這本書獨(dú)有的。源代碼在最后的附錄當(dāng)中。
我們最先接觸到的工具就是Lisp編輯器,由于common lisp標(biāo)準(zhǔn)中并沒有規(guī)定lisp實(shí)現(xiàn)利一定要用哪一種編輯器,所以現(xiàn)在我們沒有辦法給出你編輯器的具體細(xì)節(jié)。但是大概上,能告訴你,為什么他們不同于一般的文本編輯器,為什么你應(yīng)該花時(shí)間來學(xué)習(xí)使用你的lisp實(shí)現(xiàn)提供的編輯器。
在lisp中最常見的額錯(cuò)誤就是括號(hào)錯(cuò)誤。因此幾乎是一定要的就是在定義函數(shù)的時(shí)候每一次都檢查括號(hào)的數(shù)量和配對(duì)?!狤laine Gord
上面的引言是寫在25年前,那時(shí)候lisp程序員還是在打孔紙帶上寫程序?,F(xiàn)在,當(dāng)然我們使用交互式的編輯器。lisp編輯器不是普通的文本編輯器:編輯器“理解”lisp程序的語法。在我的機(jī)器上,無論什么時(shí)候我鍵入右括號(hào),編輯器都會(huì)高亮左括號(hào)。這個(gè)功能防止了我在輸入表達(dá)式的時(shí)候發(fā)生括號(hào)錯(cuò)誤。另一個(gè)編輯器的工作就是自動(dòng)縮進(jìn)。如果一個(gè)函數(shù)占用多行,那么就會(huì)自動(dòng)縮進(jìn)來規(guī)范格式以增加可閱讀。
Some of the earliest Lisp books were written before anyone thought of
systematically indenting programs to make them readable. A program that
would have been written this way back then:
一些早期的lisp書籍是在系統(tǒng)縮進(jìn)這想法出現(xiàn)之前寫就的,那時(shí)候的程序就是這樣
現(xiàn)在自動(dòng)縮進(jìn)之后就會(huì)變成這樣
還有兩個(gè)好功能室lisp編輯器可以提供的,一個(gè)是在編輯表達(dá)式的時(shí)候就對(duì)表達(dá)式求值。你可以用鼠標(biāo)點(diǎn)擊幾次定義的函數(shù),在不離開編輯器的情況下就可以對(duì)表達(dá)式求值。第二個(gè)就是,對(duì)于在線文檔的快速查看。假如我想要看任何lisp函數(shù)或者函數(shù)的文檔,我可以用幾次擊鍵來喚出。編輯器本身支持在線文檔。
Common Lisp標(biāo)準(zhǔn)定義了lisp實(shí)現(xiàn)和編輯器之間的接口。這個(gè)接口就是一個(gè)叫做ED的函數(shù)。在頂級(jí)提示符的狀態(tài)鍵入(ED)將會(huì)進(jìn)入編輯器。更多的lisp實(shí)現(xiàn)會(huì)提供更加快捷的方式,擊鍵Control-E。ED也可以接受參數(shù)來進(jìn)行操作。
第三章 進(jìn)階話題
3.16 沒有參數(shù)的函數(shù)
Suppose we wanted to write a function that multiplies 85 by 97. Notice that
this function requires no inputs; it does its computation using only
prespecified constants. Since the function doesn’t take any inputs, when we
write its definition, it will have an empty argument list. The empty list, of
course, is NIL. Let’s define this function under the name TEST:
假設(shè)我們想要寫一個(gè)函數(shù)把85和97相乘。請(qǐng)注意這個(gè)函數(shù)是沒有輸入的,他計(jì)算的只是之前已經(jīng)有的常量。既然沒有輸入,那么參數(shù)列表就是空的??樟斜懋?dāng)然也就是NIL,我們來定義這個(gè)叫做test的函數(shù):
(defun test () (* 85 97))
這之后,我們會(huì)看到
(test) → 8245
(test 1) → Error! Too many arguments.
test是一個(gè)函數(shù),所以必須使用括號(hào)來調(diào)用,字符串test在沒喲括號(hào)的情況下會(huì)被解釋為變量的引用。
test → Error! TEST unbound variable.
3.17 特殊函數(shù)quote
quote函數(shù)是一個(gè)特殊函數(shù),他并不對(duì)自己的輸入進(jìn)行求值,而是把輸入簡(jiǎn)單的返回作為輸出。
(quote foo) → foo
(quote (hello world)) → (hello world)
早期版本的lisp使用quote而不是單引號(hào)來表示不對(duì)一些對(duì)象求值,也就是本來寫成這樣的
(cons ’up ’(down sideways))
在早期版本中寫成這樣的
(cons (quote up) (quote (down sideways)))
在現(xiàn)代lisp系統(tǒng)中,使用單引號(hào)作為quote函數(shù)的標(biāo)記。然而在內(nèi)部,系統(tǒng)會(huì)將單引號(hào)轉(zhuǎn)換為quote函數(shù)。我們?cè)谑褂枚嘀氐膓uote函數(shù)的時(shí)候可以看到這點(diǎn)。第一層quote會(huì)被求值過程使用,其余的單引號(hào)會(huì)被保留下來。
’foo → foo
’’foo → ’foo also written (quote foo)
(list ’quote ’foo) → (quote foo) also written ’foo
(first ’’foo) → quote
(rest ’’foo) → (foo)
(length ’’foo) → 2
3.18 字符串的內(nèi)部結(jié)構(gòu)
到現(xiàn)在為止我們已經(jīng)通過他們的名字介紹了一些字符串,但是字符串在Common Lisp中實(shí)際上是組合對(duì)象,也就是說字符串是由不同的部分組成的。概念上,一個(gè)字符串有五個(gè)指針的組合組成,其中一個(gè)指針就是字符串的名字。其他的部分將在稍后定義。字符串fred的內(nèi)部結(jié)構(gòu)看起來如下:
上圖在引號(hào)中出現(xiàn)的fred叫做字符串(string)。有關(guān)strings是一個(gè)字符的序列,我們會(huì)在接下來的第九章詳細(xì)介紹?,F(xiàn)在他滿足的是儲(chǔ)存字符串的名字。一個(gè)字符串和字符串的名字是兩回事情。
一些字符串,想cons或者+,等等被用在lisp內(nèi)建函數(shù)的命名上。這個(gè)字符串cons在函數(shù)內(nèi)有一個(gè)指針指向編譯代碼對(duì)象(compiled code object)來表現(xiàn)機(jī)器語言指令來創(chuàng)建新的內(nèi)存單元。
當(dāng)我們畫出一個(gè)lisp表達(dá)式的矩形表達(dá)式時(shí)候,比如(equal 3 5)。常常只是畫出字符串的名字而不是展現(xiàn)內(nèi)部結(jié)構(gòu)。
我們要是展現(xiàn)更多細(xì)節(jié)的話,那么表達(dá)式(equal 3 5)將會(huì)都是這樣。
我們可以使用內(nèi)建函數(shù)symbol-name和symbol-function來提取字符串的許多部分。接下來的對(duì)話就是展現(xiàn)這個(gè),你會(huì)在自己的計(jì)算機(jī)上看見不太一樣的結(jié)果,但是意思是差不多的。
3.19 lambda 表達(dá)式
lambda表達(dá)式是由Alonzo Church,普林斯頓大學(xué)的數(shù)學(xué)家,創(chuàng)造出來的。Church想要一個(gè)清楚的方式去表示函數(shù),輸入還有計(jì)算。在lambda表達(dá)式中,一個(gè)在一個(gè)數(shù)字上加上3的函數(shù)如下所示,λ既是希臘字母lambda: λx.(3+x)
John McCarthy,lisp的創(chuàng)造者,是Church先生的學(xué)生。他通過定義函數(shù)來繼承了lambda表達(dá)式。函數(shù)λx.(3+x)在lisp中的等價(jià)形式是:(lambda (x) (+ 3 x))
函數(shù)f(x,y) = 3x+y2的lambda表達(dá)式會(huì)被寫成λ(x,y).(3x+y2),他的lisp形式就是(lambda (x y) (+ (* 3 x) (* y y)))
如所見,lambda表達(dá)式的語法和lisp是十分相似的,甚至更像defunct函數(shù),但是不想defun函數(shù),lambda并不是一個(gè)函數(shù),他更像一個(gè)想eval一樣的額制造器,我們將在第七章學(xué)習(xí)更多關(guān)于lambda表達(dá)式的內(nèi)容。
defun函數(shù)的工作室將函數(shù)名稱和函數(shù)本身連接在一起。當(dāng)鍵入一個(gè)新的函數(shù)定義,例如half。將會(huì)有兩種命名過程,第一個(gè)是,字符串half命名字符串對(duì)象,之后字符串對(duì)象指向函數(shù)。在下圖中,你可以看到字符串half指向字符串對(duì)象half。函數(shù)部分指向一個(gè)函數(shù)對(duì)象。準(zhǔn)確的函數(shù)對(duì)象是什么樣子取決于Common lisp實(shí)現(xiàn)的不同。但是正如下圖所示,可能是一個(gè)lambda表達(dá)式在某處。
當(dāng)然,lambda表達(dá)式也只是內(nèi)存單元構(gòu)成的列表。每一個(gè)lambda表達(dá)式中的字符都是五個(gè)指針組成的,比如N和/,/的叫名字哦叫做除法函數(shù),包括一個(gè)指針指向內(nèi)建的除法函數(shù)來實(shí)現(xiàn)除法功能,所以間接地,half函數(shù)指向了內(nèi)建的處罰函數(shù),圖1-3展現(xiàn)出了這些細(xì)節(jié)。
3.20 變量的作用域
變量的作用域是指變量能夠被引用的區(qū)域范圍,例如,變量n取得half函數(shù)的輸入,作用域就是half函數(shù)的函數(shù)體范圍。另一個(gè)方式去表達(dá)這個(gè)概念是說變量n是函數(shù)half的本地變量(local)。全局變量由不綁定的作用域,他們可已在任何地方被引用。
在求值回溯圖中,本地變量的作用域被限制在粗箭頭包含的創(chuàng)造變量范圍內(nèi)。在粗箭頭之外的地方變量是不能被引用的、接下來的程序就是說這個(gè)。
(defun parent (n)
(child (+ n 2)))
(defun child (p)
(list n p))
程序是有錯(cuò)誤的,我們來看看parent函數(shù)在創(chuàng)造一個(gè)本地變量n之后調(diào)用了child函數(shù),問題在哪里呢?
在求值回溯圖中,粗箭頭描繪了作用域范圍。parent函數(shù)的作用域的n只是在parent函數(shù)體中有效,在child函數(shù)的粗箭頭范圍內(nèi),是不能被引用的。所以n出現(xiàn)在child函數(shù)中被解釋為一個(gè)全局變量,沒有被賦值過。因此就會(huì)出現(xiàn)賦值錯(cuò)誤。
3.21 EVAL和APPLY
EVAL是一個(gè)lisp原始函數(shù),每一次eval的使用都給出一層計(jì)算。
’(+ 2 2) → (+ 2 2)
(eval ’(+ 2 2)) → 4
’’’boing → ’’boing
(eval ’’’boing) → ’boing
(eval (eval ’’’boing) → boing
(eval (eval (eval ’’’boing))) → Error! BOING unassigned variable.
’(list ’* 9 6)) → (list ’* 9 6)
(eval ’(list ’* 9 6)) → (* 9 6)
(eval (eval ’(list ’* 9 6))) → 54
我們不會(huì)使用在任何程序中直接使用eval,但是會(huì)間接地一直使用eval。你可以想象計(jì)算機(jī)作為一臺(tái)物理上的eval的表現(xiàn)形式。當(dāng)lisp被啟動(dòng),鍵入的每個(gè)項(xiàng)目都會(huì)被求值。
apply也是一個(gè)lisp原始函數(shù),apply把一個(gè)函數(shù)或者一系列對(duì)象作為輸入,他調(diào)用對(duì)應(yīng)的函數(shù)使用這些對(duì)象作為輸入。apply函數(shù)的第一個(gè)參數(shù)被加上#‘符號(hào)而不僅僅是加上引號(hào)’。#‘符號(hào)是一個(gè)合適的方法來把函數(shù)作為另一個(gè)函數(shù)輸入的方法。這個(gè)將在第七章給出更多解釋和細(xì)節(jié)。
(apply #’+ ’(2 3)) → 5
(apply #’equal ’(12 17)) → nil
The objects APPLY passes to the function are not evaluated first. In the
following example, the objects are a symbol and a list. Evaluating either the
symbol AS or the list (YOU LIKE IT) would cause an error.
傳給apply的對(duì)象首先并不是被求值,在接下里的例子中,對(duì)象是一個(gè)字符串和一個(gè)列表,對(duì)字符串或者列表求值會(huì)產(chǎn)生錯(cuò)誤。
(apply #’cons ’(as (you like it))) → (as you like it)
進(jìn)階話題中的函數(shù)
EVAL相關(guān)函數(shù):apply
特殊函數(shù):quote