第一部分Common Lisp介紹
第1章 介紹一下Lisp
你在學的時候覺得已經明白了,寫的時候更加確信了解了,教別人的時候就更有自信的了。一直到你開始編程的時候才明白什么是真正的理解——Alan Perlis,耶魯大學計算機科學家
本章是為了完全沒有Lisp經驗的人準備的。如果你在Lisp編程方面有足夠的自信,可以快速瀏覽這一章或者直接跳過。本章會進展的比較快,沒有編程經驗的讀者或者發現本章很難理解的讀者請去尋找一些入門類的書籍來看看,在前言里有推薦。
計算機就是允許實行計算的機器。一個字符處理程序可以處理字符,就像計算器處理數字一樣,原理都是相同的。兩個環境都是,你提供輸入(字符或者數字),并且定義一個操作(比如刪除一個字符或者加上兩個數字),之后得出一個結果(一份完整的文檔或者計算結果)。
我們稱任何計算機內存中的實體為一個可計算對象,簡稱對象。因此,字符,段落和數字都可以是對象。由于那些操作本身(刪除或者增加)也必須在計算機內存中保存,所以也是對象。
正常來說,一個計算機用戶和一個程序員的區別在于,用戶給計算機提供的是新的輸入,或者數據(字符或者數字),而程序員則是定義新的操作,或者程序,還有新的數據類型。每一個新的對象,或是數據或是程序,都必須按照之前定義的對象來定義。壞消息是,這個定義正確合理的過程可能是非常枯燥乏味的。好消息是每一個新的對象都可以在未來的對象定義中使用。因此,再復雜的程序也可以使用簡單精簡的對象來進行構建。本書包含了一些典型的AI問題,每一個問題都會慢慢分解成可管理的小部分,每一個部分都會由Common Lisp來具體描述。理想情況下,讀者會通過學習這些例子學到足夠的知識,破解新的AI問題。
讓我們來考慮一個簡單的計算例子:兩個數字的和,簡單說2+2,。如果我們手邊有一個計算器,就可以輸入2+2=,然后就會顯示答案了。在使用波蘭標志的計算器上,我們可以輸入 2 2 +來得到同樣的結果。Lisp也有計算功能,用戶可以使用交互式對話來輸入表達式,之后就可以看到答案。交互模式和其他大部分只有批處理模式的語言不同,批處理只能輸入整個程序編譯運行,然后才能看到結果。
只要輕按一下電源鍵,一個便攜式計算機就可以開始工作。Lisp程序也是需要被打開的,但是具體的細節根據機器的不同有所區別,所以我就不解釋你的Lisp如何工作了。假設我們已經成功打開了Lisp,某種的提示符就會出現。在我的計算機上,提示符就是符號“>”,這樣就顯示Lisp已經準備好接受輸入了。所以我們面對的屏幕是這樣子的:
>
現在我們輸入算式,然后看看結果。很明顯,Lisp的表達式和算術表達式有一些不一樣:一個算式由括號列表組成,開頭是操作的名字,之后是任意數量的操作數,或者參數。這種表達式叫做前綴表達式。
> (+ 2 2)
4
>
可見,Lisp打印答案,4,之后又輸出另一個提示符,>,來接受接下來的輸入。本書中的Lisp表達式的輸入都是打印字符,和用戶輸入的字符是一樣的。一般來說,程序員輸入的字符是小寫的,計算機輸出的字符都是大寫的。當然,像字符+和4是沒有區別的。
為了節省書的空間,我們有時候吧輸入和輸出放在同一行,中間用一個箭頭分割(=>),讀者可以理解為相等于,也可以是看做用于在鍵盤上按下的回車鍵,表示輸入結束。
> (+ 2 2) => 4
使用括號前綴表達式的好處之一就是括號可以清楚的標記表達式的開始和結束。如果我們想,我們可以給+添加更多的參數,他會將參數全部相加。
> (+ 1 2 3 4 5 6 7 8 9 10) =>55
接下來我們嘗試更加復雜的算式:
> (- (+ 9000 900 90 9) (+ 5000 500 50 5)) =>4444
這個例子表示表達式是可以嵌套的。函數-的參數就是括號列表,而函數+的每一個參數都是原子。Lisp表達式可能和標準的數學表達不太一樣,但是這種方式也是有好處的;表達式是由函數名后面加上參數組成的,所以+符號就不用重復出現了。比標記更加重要的是求值的規則。在Lisp中,列表的求值是首先對所有的參數求值,之后用函數操作參數,進而計算結果。這個求之規則比一般的數學求值要簡單的多,數學中的求值需要記憶很多的規則,比如在求和和求差之前必須先進行加和除操作。我們接下來會看到,真實的Lisp求值規則會有點復雜,但是不會很過分。
有時候熟悉其他編程語言的程序員,會有一些先入為主的概念,會對學習Lisp造成阻礙。對于他們,有三點需要明確,第一,其他許多語言由表達式和語句的區分,比如表達式2+2,有一個值。但是一個語句,像x=2+2,就沒有值。語句是有效果的,但是并不返回值。在Lisp中,是沒有這樣的分別的:沒一個表達式都會返回一個值,有些表達式是有效果的,但是仍然會返回一個值。
第二,Lisp的語法規則比其他語言的規則簡單。特別是標點符號更少:只有括號,引號(單引號,雙引號,反引號),空格,還有逗號作為互相之間的分隔符。因此,在其它語言中,語句y=a*x+3會被解析成七個獨立的符號,在Lisp中卻被看做一個符號。為了構成一個標記的列表,我們在記號之間插入空格(y = a * x + 3)。雖然這不是一個合法的符號列表語句,但是是一個Lisp數據的對象。第三,很多語言是用分號作為語句的結尾,Lisp不需要分號,表達式是用括號來分界的。Lisp選擇使用分號作為注釋開始的標記,分號之后的這行的所有內容都是注釋。
> (+ 2 2) ; this is a comment
4
1.1 符號計算
到現在為止,我們所做的數學計算和一個計算器能做的沒有區別。Lisp比計算器強大的地方有兩個:第一,它允許我們操作除了數字之外的其他對象。第二,后面的計算中,如果需要我們可以定義新的對象。慢慢來,一點點看看這兩個重要的特性。
除開數字之外,Lisp也可以表示字符(字母),字符的串(字符串),和任意的字符,這些個字符可以被外部解釋為任意的數學表達。Lisp也可以非原子類型的對象,也就是將多個對象包括近一個列表中宏。這項功能是語言本身的基礎功能,很好的集成完畢了;事實上,Lisp的名字由來就是列表處理的意思。
下面呢是一個列表計算的例子:
> (append '(Pat Kim) '(Robin Sandy)) => (PAT KIM ROBIN SANDY)
這個表達式將兩個名字的列表追加在一起。至于求值規則是和數字的求值一樣的規則:應用這個函數(這里是append)到后面的參數上。
之前沒有見過的,就是這個單引號('),他會對接下來要求值的表達式進行鎖定,不加修改的返回。如果不加上單引號,例如表達式(Pat Kim),就會把Pat當成一個函數名然后應用到表達式Kim上。這不是我們想象出來的過程,單引號的功能就是使得Lisp將列表當做數據來看而不是一個函數調用來看。
> '(Pat Kim) ?=> (PAT KIM)
在其他編程語言中(包括英語中),引號都是成對出現的:一個來標記開始,一個來標記結束。在Lisp中,一個單引號被用來標記表達式的開始。既然我們知道,一對括號已經用來規定表達式的開始和結束,那么后邊用來標記結束的引號就顯得多余了。引號可以用在列表,或者符號,甚至是其他任何對象上。下面是一些例子:
> 'John=> JOHN
> '(John 0 Public) => (JOHN 0 PUBLIC)
>'2 => 2
>2 => 2
> '(+ 2 2 ) => (+ 2 2 )
> (+ 2 2 ) => 4
> John => Error: JOHN is not a bound variable
> (John 0 Public) => Error: JOHN is not a function
'2求值是2是因為表達式加上引號,2求值為2是因為2本身求值就是2。一個結果,兩個原由。對比之下,john的求值,加上引號的就是輸出john,但是不加上引號的就會輸出錯誤,因為對一個符號求值意味著在系統中這個符號是有所指向的,但是之前并沒有什么對象賦值給john。
符號計算是允許互相嵌套的,甚至也可以和數字計算進行混合。下面的表達式以之前不太一樣的方式建立了一個姓名的列表,他使用了內建的函數list。之后我們會看到另一個內建函數length,他會求出列表的元素個數。
> (append '(Pat Kim) (list '(John 0 Public) 'Sandy))
(PAT KIM (JOHN 0 PUBLIC) SANDY)
> (length (append '(Pat Kim) (list '(John 0 Public) 'Sandy)))
4
關于符號有非常重要的四點需要講解:
第一Lisp對于對象的操作是不會附加任何信息的。比如,從人的視角來看,Robin Sandy很明顯就是一個人的名字,還有John Q Public是一個人名,中間名,姓的組合。Lisp是沒有這些個預設的概念的,對于Lisp來說,Robin也好xyzzy也好都只是符號而已。
第二為了進行上面的計算,必須先要了解一個Common Lisp中定義的函數,比如append,length,還有+函數。學習一門外語包括了記憶那門語言的詞匯(或者知道上哪兒查詢),還要對規范語言的基本規則和定義語義的基礎進行了解。Common Lisp提供了超過700個內建函數。具體的很多函數需要讀者去參考別的書籍,但是大部分重要的函數會在第一部分有所介紹。
第三Common Lisp是不區分大小寫的,也就是說,John還是john或者是JoHn都是一個意思,打印出來都是JOHN。在環境中,變量print-case是可以控制打印出來的大小寫的,默認是大寫的,也可以設置成小寫的。
第四構成符號的字符種類是非常繁多的:數字,字母,還有其他符號,比如加號和嘆號。符號構成的規則說起來有點小復雜,但是一般來說,構成符號的都是字母,有時候會在單詞之間用分隔符,-,或許在符號的最后會有數字結尾。有些程序猿在變量的命名方面會更加不拘小節,可能會包含類似于問號,美元符號等等的字符。例如,一個美元轉日元的程序可能會這樣命名$-to-yen或者$->yen。在Pascal或者C中,命名也許是DollarsToYen,或者dollarstoyen,dol2yen。當然這些規則之外有很多的例外,我們等他們出現的時候再解釋吧。
1.2 變量
符號計算的基本概覽之后,我們接下去看看,或許就是一門語言最最重要的一個特性:按照其他對象定義新的對象的能力,給新對象賦予名字以待后用。符號再一次扮演了一個重要的角色,用來給變量命名。一個變量可以存儲一個值,這個值可以是任何Lisp對象。給變量賦值的方法有一個就是用setf函數:
> (setf p '(John 0 Public)) => (JOHN 0 PUBLIC)
> p => (JOHN 0 PUBLIC)
> (setf x 10) => 10
> (+ X x) => 20
> (+ x (length p)) ?=> 13
在把值(John Q Public)賦值給變量p之后,我們可以使用變量的名字p來指向這個值。相似的,再把這個值賦值給變量x,我們就可以用x和p來指向這個值。
符號,在Common Lisp中也會被用來命名函數。每一個符號都可以被用來當做變量名或者函數名,或者兩者都是。例如,append和length是函數名,但是沒有作為變量的對應值,符號pi沒有對應函數但是卻又對應變量,它的值是3.1415926535897936(近似值)。
1.3 特殊形式
細心的讀者可能會發現,setf違反了求值規則。之前我們提到的函數像+,-,還有append,他們的規則是先對所有的參數求值,之后再把函數應用到參數上。但是setf沒有遵循這條規則,因為setf根本就不是一個函數。相反的,這是Lisp的基本句法,Lisp有一些為數不多的句法表達式。姑且稱之為特殊形式。在其他語言中,也有相同目的的語句存在,也確實有一些相同意義的語法標記,比如if和loop。第一,Lisp的句法形式總是在列表中的第一個,后面跟著其他的符號。Setf就是這些特殊形式中的一個,所以(setf x 10)是一個特殊表達式。第二,特殊形式的表達式會返回一個值。相比其他的語言,是有效果但是不返回值的。
在對表達式(setf x (+ 1 2))求值的過程中,我們將符號x作為變量名,并且賦值(+ 1 2),也就是3.如果說setf是一個普通函數,那么操作的順序就是先對兩個參數求值,之后再使用兩個值來進行計算,這顯然不是我們想要的效果。Setf被稱作特殊形式是因為他做的事情就是特殊的:如果沒有setf,寫一個函數給一個變量賦值就是完全不可能的事情。Lisp的核心哲學就是先提供一些為數不多的特殊形式,他們的功能是函數無法替代做到的,之后再由用戶來寫函數實現想實現的各種功能。
術語特殊形式的確是有些讓人費解,他即表示setf又表示以setf開頭的表達式。有一本書Common Lispcraft中作者就為了辨析兩個意思把setf稱作一個特殊函數,而特殊形式就用來指代setf的表達式。這種說法暗示了setf僅僅是另一種函數,一種第一個參數不求值的函數,這樣的觀點在Lisp還只是被看做一門解釋語言的時候大行其道?,F代的觀點認為setf不應該被看做是一種特殊的函數,而是應該作為一種特殊的句法標記,由編譯器來特別處理。因此,特殊形式(setf x (+ 2 1))的意思應該就是等同于C語言中的x = 2 + 1。本書中當有產生歧義的危險的時候,我們會將setf稱作一個特殊形式操作符,而表達式(setf x 3)稱作特殊形式表達式。
另外要說的是,引號僅僅是一個特殊形式的縮寫。表達式’x等同于(quote x),這個特殊形式表達式求值是x。本章使用的特殊形式操作符見下表:
特殊形式操作符 | 功能含義 |
---|---|
defun | 定義函數 |
defparameter | 定義特殊變量 |
setf | 給變量賦值 |
let | 綁定本地變量 |
case | 分支選擇 |
if | 條件選擇 |
function(#’) | 指向一個函數 |
quote(‘) | 引入常量數據 |
1.4 列表
到現在為止我們見到的列表相關函數有兩個:append和length。列表的重要性值得我們在看看更多的列表操作函數:
> P => (JOHN 0 PUBLIC)
> (first p) =>? JOHN
> (rest p) => (0 PUBLIC)
> (second p) => 0
> (third p) => PUBLIC
> (fourth p) => NIL
> (length p) => 3
函數的命名也都蠻巧妙的,first,second,third,fourth:依次返回第1234個元素。Rest的意思就沒那么明顯,意思是除第一個元素之外的后續元素,符號nil和一堆括號()的意思是完全相同的,表示一個空列表。Nil同時也用來表示Lisp中的假,也就是false的值。因此表達式(fourth p)的值就是nil,因為p本來就沒有第四個元素。請注意,列表的構成元素不一定要是原子,子列表也是可以的。
> (setf x '((1st element) 2 (element 3) ((4) 5))
((1ST ELEMENT) 2 (ELEMENT 3) ((4) 5)
> (length x) => 5
> (first x) => (1ST ELEMENT)
> (second x) => 2
> (third x) => (ELEMENT 3)
> (fourth x) => ((4))
> (first (fourth x)) => (4)
> (first (first (fourth x))) => 4
> (fifth x) => 5
> (first x) => (1ST ELEMENT)
> (second (first x)) => ELEMENT
我們已經學會怎么訪問列表內部的元素了,完完全全新建一個列表也是可以的,例子:
> P => (JOHN Q PUBLIC)
> (cons 'Mr p) => (MR JOHN Q PUBLIC)
> (cons (first p) (rest p)) => (JOHN Q PUBLIC)
> (setf town (list 'Any town 'USA)) => (ANYTOWN USA)
?> (list p 'of town 'may 'have 'already 'won!) =>
((JOHN Q PUBLIC) OF (ANYTOWN USA) MAY HAVE ALREADY WON!)
> (append p '(of) town '(may have already won!)) =>
(JOHN Q PUBLIC OF ANYTOWN USA MAY HAVE ALREADY WON!)
> P => (JOHN Q PUBLIC)
函數cons就是construct構造的縮寫。接受的參數是一個元素加一個列表。(等一下我們再看第二個參數不是列表的情況)。之后cons就會構造一個新的列表,第一個元素就是第一個參數,之后的元素就是第二個參數中的元素。函數list,接受任意數量的參數,之后會按順序形成一個列表。因此,append的參數必須是列表,而list的參數可能是列表或者原子。有一點很重要,這些函數都是創建一個新的列表,并不破壞原有的列表。表達式(append p q)的意思是創建一個完全全新的列表,用的就是p和q的元素,但是p,q本身是不會改變的。
現在我們暫且放下抽象的列表函數,來看一個簡答的問題:如果以列表的形式給出一個人的名字,怎么提取出它的家族名呢?對于(JOHN Q PUBLIC),或許可以使用函數third,但是對于滅幼中間名的名字怎么辦?Common Lisp中有一個函數叫做last;或許可以有效果,我們可以這么做:
> (last p) => (PUBLIC)
> (first (last p)) => PUBLIC
Last直接返回的不是最后一個元素本身,而是僅僅含有最后一個元素的列表。這樣的設計好像有些有悖常理,不符合邏輯,實際上是這樣。在ANSI Common Lisp中,last的定義是返回一個列表的最后n個元素,而個數這個選項的默認值就是1。因此(last p)=(last p 1)=(PUBLIC),而(last p 2)=(0 PUBLIC),這樣子定義或許是有些違背常理。所以我們才需要last和first結合來完成這個功能,獲取真實的最后一個元素。為了表示他的功能,我們給他一個名正言順,叫做last-name函數。Setf是用來定義變量名字的,所以對于函數的定義并不適用。定義函數的方法將會在下一節講到。
1.5 定義一個新的函數
特殊形式defun就是define function的縮寫。先來定義一下上一節中的last-name函數。
(defun last-name (name)
"Select the last name from a name represented as a list."
(first (last name)))
給出一個新的函數名叫做last-name。參數列表中只有一個參數,(name),意思就是函數只接受一個參數,指向的就是名字。接下來雙引號中的內容叫做文檔字符串,用來說明函數是做什么用的。文檔字符串本身并不參與計算,但是在調試和理解大型系統的時候是一個利器。函數定義的函數體就是(first (last name)),也就是之前用來獲取p的家族名的程序。不同的地方在于我們這一次是要獲取所有名字的家族名,不僅僅是p。
一般來說,函數的定義遵循下面的格式(文檔字符串是可選的,其他部分是必須的):
(defun函數的名字 (參數列表 ...)
"文檔字符串 "
函數體 ... )
函數名必須是一個符號,參數一般也是符號,函數體是由一個或者多個表達式所組成,函數被調用的時候這些歌表達式就會被求值。最后一個表達式的值返回,作為整個函數調用的值。
一個新的函數last-name定義之后,就可以像其他Lisp函數一樣來使用了:
> (last-name p) => PUBLIC
> (last-name '(Rear Admiral Grace Murray Hopper)) =>HOPPER
> (last-name '(Rex Morgan MD)) => MD
> (last-name '(Spot)) => SPOT
> (last-name '(Aristotle)) => ARISTOTLE
最后三個例子指出了編程過程當中固有的限制。當我們說定義了一個函數last-name的時候,我們并不是真的認為每一個人都是有一個姓的;僅僅是在一個列表形式的名字上的一個操作而已。憑直覺講,MD應該是一個頭銜,Spot大概是一條狗的名字,而Aristotle亞里士多德生活在姓這個概念發明出來之前的年代。但是我們還是可以不斷改進自己的程序last-name來適應這些例外的情況。
我們也可以定一個函數叫做first-name,雖然這個定義很多余(功能和first函數一樣),但是顯式重新定義也是可以的。之后就可以使用first-name來處理姓名列表,而first用來處理任意的列表,計算機的操作是完全相同的,但是我們作為程序員(或者是閱讀程序的人),會少了很多困惑。定義像first-name這樣的函數的另一個好處是,如果我們決定改變名字的表現,我們只需要更改first-name的定義。這可比在一個大型程序中更改first的應用來的方便。
(defun first-name (name)
"Select the first name from a name represented as a list."
(first name))
> p => (JOHN 0 PUBLIC)
> (first-name p) => JOHN
> (first-name '(Wilma Flintstone)) => WILMA
> (setf names '((John 0 Public) (Malcolm X)
(Admiral Grace Murray Hopper) (Spot)
(Aristotle) (A A Milne) (Z Z Top)
(Sir Larry Olivier) (Miss Scarlet)) =>
((JOHN 0 PUBLIC) (MALCOLM X) (ADMIRAL GRACE MURRAY HOPPER)
(SPOT) (ARISTOTLE) (A A MILNE) (Z Z TOP) (SIR LARRY OLIVIER)
(MISS SCARLET))
> (first-name (first names) => JOHN
最后一個表達式中,我們用的那個函數是先提取列表中的第一個元素,也就是一耳光列表,在提取這第一個元素的元素。
1.6 使用函數
One good thing about defining a list of names, as we did above, is that it makes it
easier to test our functions. Consider the following expression, which can be used to
test the 1 ast-name function:
上面定義的一大串名字的列表,接下來可以用來測試我們的函數??纯聪旅娴谋磉_式,就是用來測試last-name函數:
> (mapcar #'last-name names)
(PUBLIC X HOPPER SPOT ARISTOTLE MILNE TOP OLIVIER SCARLET)
井號加上單引號#’,這個標記是用來將符號轉換成函數名的意思。和引號的功能類似。內建函數mapcar接受了兩個參數,一個函數和一個列表。這個表達式返回一個列表,列表的每一個元素都是第二個參數中的元素經過第一個參數-函數,處理過后的結果。換句話說,mapcar調用等同于:
(list (last-name (first names))
(last-name (second names))
(last-name (third names))
.. . )
mapcar的名字來自于maps,定位,地圖的意思,這是前半部分map,意味著后續函數會操作每一個列表的元素。后半部分car指的是Lisp函數car,first函數的老名字。Cdr是rest函數的老名字。Car和cdr是一個縮寫,(contents of the address register)和(contents of the decrement register),這些名字的第一次使用是在一臺IBM 704機器上,是Lisp的第一個版本實現。我很確信你也認為first和rest是個更好的名字,而且他們會在我們關于列表的討論中一直替代car和cdr存在。但是我們在跳出列表視角,看是將兩個看做是一對值的時候,還是會使用car和cdr的。
還有更多mapcar的例子:
> (mapcar #' - '0 2 3 4) => (-1 -2 -3 -4)
> (mapcar #'+ '0 234) '00 20 30 40)) => 01 22 33 44)
最后一個例子顯示了,mapcar是可以接受三個參數的,在這種情況下,第一個參數應該是一個二元程序,也就是支持兩個參數的輸入,之后就可以處理對應的元素。一般來說mapcar會希望n元函數作為第一個參數,之后是n個列表。之后的操作就是函數收集每一個列表的元素,依次來。之后一個個處理,知道其中一個列表到了盡頭。Mapcar就會返回一耳光所有函數返回值的列表作為結果。
現在我們理解了mapcar,接下來測試一下first-name函數:
> (mapcar #'first-name names)
(JOHN MALCOLM ADMIRAL SPOT ARISTOTLE A Z SIR MISS)
這些歌結果可能讓人比較失望,因為有很多不正常的結果在。假如我們想要去掉那些頭銜或者稱呼,miss或者Admiral,做一個真正的名字的輸出版本。記下來可以這么修改:
(defparameter *titles*
'(Mr Mrs Miss Ms Sir Madam Dr Admiral Major General)
"A list of titles that can appear at the start of a name.")
最新引入的特殊形式操作符叫做defparameter,用來定義一個參數,也就是一個變量,在計算過程中不改變。但是我們要加新東西的時候會更新(比如加上法語Mme或者軍銜Lt.)。defparameter同時給出變量名和值,定義的變量之后的程序就可以使用。在這個例子中我們練習的選項就是提供一個文檔字符串來描述變量。在Lisp程序員之間有個慣例就是將特殊變量的名字用星號給包裹起來,這也僅僅是一個慣例而已;在Lisp中,星號僅僅是一個字符,沒有特別的含義。
我們接下來定義一個新的first-name版本,代替之前的版本。這個版本的改進僅僅是我們可以更改變量的值,也可以改變函數的值。這個定義的邏輯是,如果名字的第一個單詞是titles列表的一個成員的話,就會忽略這個單詞,返回后面的first-name部分。否則,我們像之前一樣使用第一個元素的話。另一個內建函數member,就會檢測,如果第一個參數是列表的一部分,就會將第二個參數傳進去。
特殊形式if的語法是這樣(if 測試條件 then部分 else部分)。在Lisp中有很多特殊形式是用來做條件測試的;if是本例子中最合適的。If語句的求值順序是最先求值測試部分。如果為真,那么then部分就會被求值并且作為if語句的值返回。如果測試部分為假,那么else部分就會被求值,之后返回。有一些語言堅持說是if語句的測試部分的值必須是true或者是false,Lisp對待這個問題寬容很多。測試部分求值的結果任何都是合法的。只有值nil被認為是false的值;其他所有的值都看做是true。在下面first-name的定義中,如果第一個元素是titles中的,函數member會返回一個非nil的值(true)。如果不是,就會返回nil(false)。雖然所有的非nil值都被看做是true,按照慣例常量t一般是用來表示真。
(defun first-name (name)
"Select the first name from a name represented as a list."
(if (member (first name) *titles*)
(first-name (rest name))
(first name)))
當我們使用一個新的first-name版本的時候,結果會更好一些。另外,函數的操作會一次性把很多的前綴去掉。
> (mapcar #'first-name names)
(JOHN MALCOLM GRACE SPOT ARISTOTLE A Z LARRY SCARLET)
> (first-name '(Madam Major General Paula Jones)
PAULA
通過追蹤first-name的執行過程,我們可以看到程序是如何運行的,都有那些值輸入,以及輸出了哪些值。特殊形式trace和untrace就是這個用處的。
> (trace first-name)
(FIRST-NAME)
> (first-name '(John Q Public))
(1 ENTER FIRST-NAME: (JOHN Q PUBLIC))
(1 EXIT FIRST-NAME: JOHN)
JOHN
當first-name被調用,按照定義就是單個參數的輸入,一個名字,值是(JOHN Q PUBLIC),最終返回的值是JOHN。Trace打印的兩行信息是顯示函數的輸入和輸出,之后是Lisp打印的最終結果JOHN。
下一個例子更加復雜一些,函數first-name被調用了四次。第一次,輸入的名字是(Madam Major General Paula Jones)。因為第一個元素是Madam,是titles的元素之一,結果就是再一次調用first-name,輸入的就是剩下的部分(Major General Paula Jones)。過程反復了兩簇,最終輸入的名字是(Paula Jones)。Paula不是titles的元素,就成為了first-name的結果,也就是這四次調用的結果。Trace是開啟追蹤,也可以用untrace來關閉追蹤。
> (first-name '(Madam Major General Paula Jones)) =>
(1 ENTER FIRST-NAME: (MADAM MAJOR GENERAL PAULA JONES))
(2 ENTER FIRST-NAME: (MAJOR GENERAL PAULA JONES))
(3 ENTER FIRST-NAME: (GENERAL PAULA JONES))
(4 ENTER FIRST-NAME: (PAULA JONES))
(4 EXIT FIRST-NAME: PAULA)
(3 EXIT FIRST-NAME: PAULA)
(2 EXIT FIRST-NAME: PAULA)
(1 EXIT FIRST-NAME: PAULA)
PAULA
> (untrace first-name) => (FIRST-NAME)
> (first-name '(Mr Blue Jeans)) => BLUE
First-name函數可以被稱作是遞歸的,因為它的函數定義中包含了對自身的調用。第一次接觸遞歸這個概念的程序員或許認為他很神秘。但是遞歸函數事實上和非遞歸函數沒有區別。任何函數對于給定的輸入都會返回正確的值。對于這句話的理解可以拆成兩部分來看,一個函數必須返回一個值,函數不能返回任何錯誤的值。兩句話等價于前面的一句話,但是思考起來就更加容易,程序的設計也更加方便。
接下來我說明一下first-name問題的抽象描述,突出一下函數的設計,說明一個事實,遞歸的解決方案并不以任何方式和Lisp綁定的。
function first-name(name);
if 名字的第一個元素師title的元素
then 搞點復雜的事情包first-name找出來
else 返回第一個元素
這把整個問題剖開成了兩個部分。在第二部分中,直接返回答案,并且就是正確答案,我們還沒有定義第一部分的該做些什么。但是我們知道答案應該就在第一個元素之后的列表中,我們要做的就是對后面的列表進行操作。這部分就是說再一次調用first-name,即使還沒有完成所有的定義。
function first-name(name);
if 名字的第一個元素師title的元素
then 將first-name應用到名字的剩余部分
else 返回第一個元素
現在,first-name的第一部分就是遞歸的,第二部分仍然沒有改變。第二部分會返回正確的值,這是我們確信的,第一部分返回的值只是first-name返回的。所以first-name作為一個整體反悔了正確的答案。因此,對于求名字的答案,我們算是做了一半了,另外一半就是要看返回的一些答案了。但是每一個遞歸調用都會砍掉第一個元素,在剩下的中尋找,所以對于n個元素的列表至多有n重遞歸調用。這樣函數的正確就完整了。深入學習之后,遞歸的思想就不是一個令人困惑的謎題,而是一種有價值的思想。
1.7 高階函數
函數步進可以被調用或者操作參數,還可以像其他對象一樣被操作。接受一個函數作為參數的函數被稱作高階函數。之前的mapcar就是一例。為了顯示高階函數的編程風格,我們來定義一個新的函數叫做mappend。接受兩個參數,一個函數一個列表。mappend將函數定位到每一個列表的元素上,然后將他們追加在一個結果中。第一個定義使用了apply函數,會把指定的函數應用到參數列表中。
(defun mappend (fn the-list)
"Apply fn to each element of list and append the results."
(apply #'append (mapcar fn the-list)))
現在我們嘗試理解apply和mappend是如何工作的。第一個例子是將加函數應用到四個參數上。
> (apply #'+ '(123 4)) => 10
下一個例子是將append應用在兩個參數的列表上,每一個參數都是列表,如果參數不是列表,就會報錯。
> (a pp 1 y #' append ' ((1 2 3) (a b c))) => (1 2 3 ABC)
我們現在定義一個新函數self-and-double,引用到多個參數上。
> (defun self-and-double (x) (list x (+ x x)))
> (self-and-double 3)=> (3 6)
> (apply #'self-and-double '(3)) => (3 6)
如果我們給self-and-double輸入超過一個參數,或者輸入不是數字的話,就會報錯。對表達式(self-and-double 3 4)或者(self-and-double 'Kim)求值就會報錯。現在,讓我們回到定位函數。
> (mapcar #'self-and-double '(1 10 300)) => ((1 2) (10 20) (300 600))
> (mappend #'self-and-double '(1 10 300)) => (1 2 10 20 300 600)
給mapcar傳遞一個三個參數的列表,結果總是三個元素的列表。每一個值及時調用函數產生的結果。相對的,mappend被調用的時候,返回的是一個大列表,就相當于mapcar的所有都是在一個追加列表中。如果給mappend傳遞的函數不是返回列表的話,會報錯,原因是append要求他的參數是列表。
現在考慮這樣一個問題:給定一個列表,返回的列表是由原始列表中的數字和這些數字的負數組成的列表。例如輸入是(testing 1 2 3 test)就返回 (1 -1 2 -2 3 -3)。這個問題用mappend做組件很容易就解決了。
(defun numbers-and-negations (input)
"Given a list, return only the numbers and their negations."
(mappend #'number-and-negation input))
(defun number-and-negation (x)
"If x is a number, return a list of x and -x."
(if (numberp x)
(list x (- x))
nil ) )
> (numbers-and-negations '(testing 1 2 3 test)) => (1 -1 2 -2 3 -3)
下面mappend的可選定義并沒有使用mapcar,代替的是一次構建一個元素。
(defun mappend (fn the-list)
"Apply fn to each element of list and append the results."
(if (null the-list)
nil
(append (funcall fn (first the-list))
(mappend fn (rest the-list)))))
Funcall類似于apply,他接受函數作為第一個參數,然后將函數應用到后面的參數中,但是在funcall中,后面的參數是獨立列出的。
> (funcall #' + 2 3) => 5
> (apply #'+ '(2 3)) => 5
> (funcall #' + '(2 3)) => Error: (23) is not a number.
這幾個表達式分別等價于(+ 2 3), (+ 2 3),和(+ ’(2 3))。
到現在為止用的函數,要么是Common Lisp預定義好的,要么是defun引入的,都是有名字的,沒有名字就可以使用的函數也是有的,需要介紹特殊句法lambda。
Lambda這個名字來自于數學家阿隆佐邱奇發明的函數表達法。Lisp一般是傾向于使用簡潔的希臘字母,但是lambda是一個例外。更加貼切的名字應該是叫make-function。Lambda表達式是從Russell和Whitehead合著的《數學原理》中的表達法導出得來,他在綁定變量的上面加上了一個標記符號。邱奇想要的是一個一維的字符串,所以他將標記符移動到了表達式的前面”x(x + x),在標記的下面什么都沒有是比較搞笑,所以邱奇就把這個符號替換成了lambda字母,但是lambda的大寫希臘字母很容易和其他字母搞混,一般是用小寫的lambda放在前面。約翰麥卡錫曾經是邱奇教授在普林斯頓的學生,所以當麥卡錫在1958年發明Lisp的時候,他繼承了lambda標記法。在鍵盤上沒有希臘字母的時代,麥卡錫就用lambda來表示,一直延續到了今天。一般來說一個lambda表達式是這樣子的:
(lambda (參數 ...) 主體 ...)
一個lambda表達式是一個函數的非原子式名字,就像append是一個內建函數的原子式名字。這樣子的匿名函數,第一次使用就是在調用的位置進行調用,但是如果我們想要在函數中調用的話,還是需要加上#’符號。
> (lambda (x) (+ x 2)) 4) => 6
> (funcall #'(lambda (x) (+ x 2)) 4) => 6
為了理解兩者之間的差別,我們必須要搞清楚表達式是究竟如何求值的。求值的正常規則是這樣:對所有的符號求值為所指向的對象。所以在(+ x 2)中的x求值的結果是名字為x的變量的值。列表的求值是兩種方式之一,如果列表中的第一個元素是特殊形式操作符,之后的列表就根據特殊形式的語法規則進行求值。否則,列表就解釋成一個函數調用。作為函數,第一個元素以一種獨特的方式求值。這就意味著,他就是一個符號或者是一個lambda表達式。無論哪一種,第一個元素的名字的函數都會對后面的參數求值后的結果操作。這些值是有正常求值規則決定的。如果我們想要指向一個除了第一個元素的調用意外的位置,就需要使用#’,否則表達式就會用正常的求值規則,也不會被看做是函數。例如:
> append => Error: APPEND is not a bound variable
> (1 ambda (x) (+ x 2)) => Error: LAMBDA is not a function
還有一些正確使用函數的例子:
> (mapcar #'(lambda (x) (+ x x)
'(1 2 3 4 5)) =>
(2 4 6 8 10)
? > (mappend #'(lambda (1) (list 1 (reverse 1)))
'((1 2 3) (a b c))) =>
(1 2 3) (3 2 1) (A B C) (C B A))
有時候使用其他編程語言的程序員還不能使用lambda表達式來看問題。Lambda表達式很有用的理由有兩個:第一,對于一些邊角料一般的程序沒必要專門分配一個名字。比如對于表達式(a+b )*( c+d),在程序中需要,但是沒有必要一定要加上一個temp或者temp2這樣的名字來存儲。使用lambda就可以讓代碼更賤清楚一些,不用再找一個名字了。
第二點更重要的是,lambda表達式使得在運行時創建函數稱為可能。這種強大的技術在大部分的編程語言中是不可能實現的。這些運行是函數,被稱為閉包,將會在3.16小節介紹。
1.8 其他數據類型
到現在為止,我們只見到了四種Lisp對象:數字,符號,列表和函數。Lisp實際上定義了25中不同類型的對象:向量,數組,結構,字符,流,哈希表,等等。這里我們再引入一個,字符串。你會在之后看到,字符串和數字一樣,是求值為自身的。字符串主要用在打印信息,符號則是主要用在與其他對象的關系,變量命名。字符串的打印形式是在兩邊都會有一個雙引號。
> "a string" => "a string"
> (length "a string") => 8
> (length "") => 0
1.9 總結:Lisp的求值規則
現在我們總結一下Lisp的求值規則:
每一個表達式,不是列表就是原子
每一個待求值的列表,不是特殊形式表達式就是一個函數應用
特殊形式表達式的定義,就是第一個元素是特殊形式操作符的列表。表達式的求值遵循的是怪異的求值規則。例如,setf的求之規則就是:第二個參數正常求值,將第一個參數賦值,然后返回那個值。Defun的規則是定義一個西函數,返回函數的名字。Quote的規則是返回不求值的第一個參數。標記’x實際上就是quote函數的縮寫。相似的,標記#’f是特殊形式表達式(function f)的縮寫
'John = (quote John) => JOHN
(setf p 'John) => JOHN
(defun twice (x) (+ x x)) => TWICE
(if (= 2 3) (error) (+ 5 6) => 11
函數應用的求值規則:首先對列表第一個元素之外的所有參數求值,之后找到第一個元素對應的函數,應用在參數上。
(+ 2 3) => 5
(- (+ 90 9) (+ 50 5 (length '(Pat Kim)))) => 42
請注意如果'(Pat Kim)沒有引號的話,會被當做函數應用來處理。
每一個原子,不是非符號就是符號。(這里相當于廢話,原文是a symbol or an nonsymbol,我的理解是符號是原子,不能進行破拆得對象也就是具有原子的特性。比如字符串,任何求值為自身的數字,字符串)
符號被求值出來的值就是變量名最近被賦值的那個值。符號由字母組成,可能有數字,極少會有標記符號。為了避免歧義,我們使用的符號大部分是字母字符組成,只有少數例外。例外比如是全局變量,是用星號包裹的。
names
p
*print-pretty*
非符號原子是求值為自身?,F在為止,我們所知的只有數字和字符串是非符號原子。數字是由數組成的,可能還有十進制點和符號。另外的一些支持是科學記數法,分數,負數,還有不同進制的數字。字符串是由雙引號包裹的字符。
42 ?=> 42
-273.15 ?=> -273.15
"a string" => "a string"
還有一些小細節會讓定義變得復雜一些,但是這里這樣的定義足夠了。
對于Lisp初學者來說,引起困惑的其中一點就是讀取表達式和求值表達式的區別。初學者在輸入的時候經常想像:
> ( + (* 3 4 ) (* 5 6))
會這樣想像,首先機器讀取(+,知道是加函數,之后就會讀取(* 3 4)計算出值是12,之后讀取(* 5 6)計算出值是30,最后計算出值是42。事實上,機器真正的行為是一次性讀取了整個表達式。列表(+ (* 3 4) (* 5 6))。在被讀取之后,系統才開始求值。求職的過程可以用解釋器看到列表顯示,或者用編譯器來翻譯成機器碼指令,之后執行。
之前我們的描述不是很準確,說,數是由數字組成,可能還會有十進制小數點和符號。準確的說法應該是這樣,一個數字的打印形式,是函數讀取的形式也是函數打印的形式,是由數字組成,可能還有十進制小數點和符號。數字在機器內部的行書根據機器不同而不同,但你可以確信的是在內存的特定位置會有一個bit位的模式存在,內部的數字自然是不包含打印的字符的十進制形式。相似的,字符串的打印形式是用雙引號括起來;它的內部形式是一個字符向量。
初學者對于表達式的讀取和求值可能已有了比較好的理解,但是對于表達式求值的效率了解仍然不多。有一次一個學生使用了一個單字母的符號作為變量名,因為他覺得計算機檢索一個字母會比檢索多個字母快一些。事實上,短的名字在讀取的過程是會快一些,但是在求值中是沒有區別的。每一根變量,不管名字是什么樣的,都僅僅是一個內存位置,內存的訪問和變量名字是無關的。
1.10 是什么造就了Lisp的與眾不同?
是什么讓Lisp區別于其他編程語言?為什么Lisp是一個適用于AI的編程語言?下面主要是說了八點重要的因素:
對列表的內建支持
自動存儲管理
動態類型
頭等函數
統一的語法
交互式環境
可擴展性
歷史悠久
總的來說,這些因素可以讓程序猿慢慢做決定。舉個例子,對于變量的命名來說,我么可以使用內建的列表函數來構造和操作名字,而不用顯式地決定變量名字的展現是什么樣子。如果我們決定改變展現,回頭更改程序的一部分是很容易的,其他部分不用修改。
這種延遲決定的能力,或者更加精確點說,做出臨時的非綁定的決定的能力,通常是一件好事,因為這就意味著一些不恰當的細節可以被忽視了。當然延遲做決定的負面影響也是有的。首先,我們給編譯器的信息越少,就會有更大的幾率產生很多低效率的代碼。第二,我們告訴編譯器越少,編譯器給出的前后不一致或者警告就會越少。錯誤的發生可能會延遲到運行狀態。我們會深入的考慮每一個因素,權衡每一點的利弊:
列表的內建支持
列表,是一個非常豐富多彩的結構,在任何語言中都會有列表的實現,Lisp是讓他變的更加易用。很多AI應用程序包含了常態可變大小的列表,定長的數據結構,類似于向量是比較難用的。
再起的Lisp版本將列表作為他們唯一的內聚數據結構。Common Lisp也提供了其他的數據結構,因為列表并不總是最高效的選擇。
自動內存管理
不需要關心內存位置的細節,試下自動內存管理可以給程序員省下很多精力,也會讓函數式編程更加方便。其他的語言則會給程序員一些選擇。變量的內存位置是在棧中,意思是過程開始的時候被分配,過程結束就會銷毀,這是比較有效率的方式,但是卻排除了函數會返回復雜值的可能性。另一個選擇,就是顯式地分配內存并且手動釋放。也可以用函數式編程但是可能會導致錯誤發生。
舉個例子,計算表達式a * (b + c),abc都是數字。其實用任何語言都能實現這個計算,下面是Pascal和Lisp的代碼:
/* Pascal */
a * ( b + c )
;;;Lisp
( * a ( + b c))
他們之間唯一的區別就是Pascal使用的是中綴表達式,而Lisp使用前綴表達式?,F在,我們把abc都替換成矩陣。假設我們已有矩陣的加法和乘法過程。在Lisp中表達式形式是和上面完全一樣的;只有函數名變化了。在Pascal中,我們需要聲明臨時變量來保存棧中的中間結果,之后用一系列的過程調用替換函數表達式:
/* Pascal */
var temp, result: matrix;
add(b,c,temp);
mult(a,temp,result);
return(result);
;;;Lisp
(mult a (add b c))
用Pascal實現的另一種方式是在堆內存中分配矩陣的空間。之后就可以和Lisp使用一樣的表達式了。然而,實踐中卻不會這么做,因為這需要顯式地管理內存。
/* Pascal */
var a,b,c,x,y: matrix;
x := add(b,c);
y := mult(a,x);
free(x);
return y;
一般來說,對于Pascal程序員,選擇銷毀哪一個數據結構是很艱難的決定,如果搞錯了,就很容易造成內存溢出。更糟的話,程序員如果銷毀了還在使用的結構,之后對這塊內存的再分配就會報錯。Lisp自動分配和銷毀結構,所以都沒有這些問題。
動態類型
Lisp程序員不需要給出變量的類型聲明,因為語言本身會在運行時確定每一個對象的類型,而不是在編譯時指定。這會讓Lisp程序更簡短,開發更快,也可以使得原來并沒有某項功能的函數,擴展成適應特定對象的函數。在Pascal中,我們可以寫一個函數來對一百個整數的數組進行排序,但是我們不能將這個過程用在200個整數的數組或者100個字符串的排序。在lisp中,一個函數適應所有對象。
理解這個優點的一個方式就是看看在其他語言中,實現這靈活性有多難。Pascal是不可能的;事實上轉為修正Pascal的問題而發明的Modula語言也是不可能。Ada語言的設計考慮了靈活的通用函數,但是Ada的方案還不是很理想:他用過長的篇幅來實現Lisp中的簡短功能,并且Ada的編譯器也是過于冗雜。(反正就是黑其他語言)。
換句話說,動態類型的缺點,就是一些錯誤會待到運行時才會被探測出來。強類型語言的一個巨大的好處就是在編譯時錯誤就可以檢測出來。強類型語言的一大失敗之處也是只能找出一小部分的錯誤。編譯器可以告訴你諸如將字符串誤傳入數字函數中這樣的問題,但是他不會發現,將奇數傳入需要偶數的函數這類問題。
頭等函數
頭等,頭等對象的意思就是可以再任何地方使用,以操作其他對象相同的方式進行操作的對象。在Pascal或者C中,函數可以作為參數傳遞給另一個函數,但是他們不是頭等的,因為在運行的時候不可以創建一個新的函數,也不可以創建一個沒有名字的匿名函數。在Lisp中我們可以使用lambda表達式,這個會在3.16小節解釋。
統一的語法
Lisp程序的語法簡單易學,打字錯誤易糾正。另外可以寫程序操作其他程序或者定義一種全新的語言-這是一個強大的技術。簡單的語法使得Lisp易于分析。要使用的編輯器應該支持自動縮進還有括號匹配。不然看Lisp程序就頭大了。
當然了,有些人是反對一切都訴諸括號。對于反對有兩個回應,第一,換個角度想想看,如果Lisp是用所謂傳統的語法,括號對就會用什么來替代呢?在算術或者邏輯運算的條件下,就需要一個隱式的運算符優先級,在控制流的情況下就需要一個開始結束的標記存在。但是這兩個都不一定是優點。隱式的優先級制度是惡名昭彰的錯誤易發地帶,而開始結束標記僅僅是給代碼徒增空間,沒有實際的意義。很多語言都從開始結束標記脫離了:C語言就是用了花括號{},效果和括號一樣。一些現代的函數式語言(比如Haskell)就是用橫向空格符號,沒有任何顯式的組織。
第二,很多Lisp程序員都考慮過,用預處理器將傳統的語法翻譯成Lisp語法。但是沒有一個最終流行的。并不是程序員們覺得Lisp的括號能忍,而是找到了括號的好,相信你用過一段也會覺得好的。
還有一點很重要的就是Lisp數據的語法和程序的語法是一樣的。顯然,將數據轉化成程序是非常方便的。更好的是,直接省掉了讓通用函數處理I/O的時間,Lisp函數的讀取和打印都是自動處理任何的列表,結構,字符串或者數字。這樣開發過程中單個函數的測試就很平常了。在傳統語言,比如C或者Pascal,你會寫一個特定目的的函數來讀取打印每一個對象,以做到調試的目的,也有一些特殊目的的驅動來做這件事情。由于又花時間又容易出錯,往往是避免測試的。Lisp的特性使得測試更加容易,開發更加快捷。
交互式環境
傳統上來說,一個程序員會先寫一個完整的程序,編譯,修正編譯器報出的錯誤,之后運行調試。這個過程就是批處理交互。對于很龐大的程序,編譯器的等待時間就占用了調試的很大一部分。在Lisp中一般是一次寫一個小函數,馬上就從求值系統中獲得反饋。這種方式就是交互式環境。只有更改過的函數需要重新編譯,所以等待的時間大大縮短了。另外,Lisp程序員可以在任何時候輸入任意的表達式調試。
請注意,交互式和批處理語言的概念和解釋型語言與編譯型語言是不一樣的。這兩者經常被搞混,Lisp的價值在于是一門交互式語言。實際上有經驗的Common Lisp程序員傾向于使用編譯器。重點在于交互式而不是解釋型。
交互式環境的概念變得流行起來,甚至傳統的語言,C和Pascal都開始提供交互式版本,所以這已經不是Lisp獨有的優勢了。一個C解釋器可能會允許程序員輸入表達式,之后馬上求值反饋,但是不會允許寫函數,比如,便利符號表,找出用戶定義的函數,然后打印信息。在C中,甚至是解釋的C中,符號表都僅僅是為了適應解釋器的無用發明,程序結束就銷毀。在Lisp中,符號表是頭等對象,都是由函數來進行訪問維護的。
Common Lisp提供了一個極其豐富的工具集,包括了超過700個內建函數(ANSI Common Lisp提供了900多個)。因此,寫程序更多的是對已有的代碼進行堆砌,寫一些原創的新代碼會少一些。除開標準函數,Common Lisp的實現一般會提供交互式編譯器擴展,調試器和圖形窗口。
可擴展性
在Lisp發明的1958年,沒有人預見到,在過去的幾十年間,編程語言設計和編程理論會有如此巨大的發展。早期的其他語言已經退出歷史的舞臺,取而代之的是基于更新觀念的語言。但是Lisp延續了下來,因為它本身的適應能力。Lisp是可擴展的,面對新的流行特性,他在不斷的演進自己。
擴展語言最簡便的方法就是宏,在語言架構中,case和if-then-else結構就是宏來實現的。但是Lisp的靈活性遠不止如此,全新的編程風格也能簡單實現。很多AI應用程序是基于規則編程的概念上的,其他的編程風格,比如面向對象,也用Common Lisp對象系統(CLOS)來實現了,宏冪函數數據類型的集合已經整合進了ANSI Common Lisp中。
下面的例子展示了Lisp已經在前言走了多遠:
(PROG (LIST DEPTH TEMP RESTLIST)
(SETQ RESTLIST (LIST (CONS (READ) 0)) )
A (COND
((NOT RESTLIST) (RETURN ‘DONE))
(T (SETQ LIST (UNCONS (UNCONS RESTLIST
RESTLIST)DEPTH))
(COND ((ATOM LIST)
(MAPC ‘PRIN1 (LIST ‘”ATOM:” LIST ‘”,” ‘DEPTH DEPTH))
(TERPRI))
(T (SETQ TEMP (UNCONS LIST LIST))
(COND (LIST
(SETQ RESTLIST (CONS(CONS LIST DEPTH) RESTLIST))))
(ADD1 DEPTH)) RESTLIST))
))))
(GO A))
請注意,這里有一個現在已經被摒棄了的go語句,程序也缺少足夠的縮進(附注,其實是markdown的代碼語法不支持縮進,我嘗試了很多代碼的表現,圖片啦,加粗啦,最后這個反引號算是效果還不錯了,只是沒辦法行內縮進。)用遞歸實現的版本如下:
(PROG NIL (
(LABEL ATOMPRINT (LAMBDA (RESTLIST)
(COND ((NOT RESTLIST) (RETURN ‘DONE))
((ATOM (CAAR RESTLIST)) (MAPC ‘PRIN1
(LIST ‘”ATOM:” (CAAR RESTLIST)
‘”,” ‘DEPTH (CDAR RESTLIST)))
(TERPRI)
(ATOMPRINT (CDR RESTLIST)))
(T (ATOMPRINT (GRAFT
(LIST (CONS (CAAAR RESTLIST) (ADD1 (CDAR RESTLIST))))
(AND (CDAAR RESTLIST) (LIST (CONS (CDAAR RESTLIST)
(CDAR RESTLIST))))
(CDR RESTLIST )))))))
(LIST (CONS (READ ) 0))))
這兩個版本都很難閱讀,使用現代的眼光來看(文本編輯器和自動縮進),更簡單的版本也可以實現。
(defun atomprint (exp &optional (depth 0))
“print each atom in exp. Along with its depth of nesting.”
(if (atom exp)
(format t “~&ATOM: ~a,DEPTH ~d” exp depth)
(dolist (element exp)
Atomprint element (+ depth 1))))
1.11 練習題
【m】1.1 定義一個last-name來處理"Rex Morgan MD," "Morton Downey, Jr.,"
【m】1.2 定義一個取冪的函數,將數字乘n次方,例如(power 3 2)就是3的2次方,9。
Write a function that counts the number of atoms in an expression.
For example: (count-atoms '(a (b) c)) = 3. Notice that there is something of an
ambiguity in this: should (a nil c) count as three atoms, or as two, because it is
equivalent to (a () c)?
【m】1.3 寫一個函數來計算表達式中原子的數量。例如,(count-atoms '(a (b) c)) = 3。有一點需要辨明,表達式(a nil c)的原子數量是算作三個還是兩個?因為這個表達式等于(a () c)。
【m】1.4 寫一個函數來計算一個表達式中出現的另一個表達式的次數。例如:
(count- anywhere 'a '( a (a) b) a)) => 3
【m】1.5 寫一個函數來計算兩個數字序列的點積(笛卡爾乘積)。
(dot-product '(10 20) '(3 4) = 10 x 3 + 20 x 4 = 110