Common LISP Hints
出自Ubuntu中文
|
[編輯] 關於Common LISP Hints
作者:Geoffrey J. Gordon <ggordon@cs.cmu.edu>
修訂:Bruno Haible <haible@ma2s2.mathematik.uni-karlsruhe.de> 與 Peter Van Eynde <s950045@uia.ua.ac.be>
Friday, February 5, 1993
注:本 Common Lisp 教學文件是針對 CMU 版本的 Lisp ,所以使用者之間可能會因為採用的 Lisp 版本不同,在執行細節上有些微差異。
[編輯] 更多信息
據我所知到最好的 Lisp 教科書是 Guy L. Steele Jr. 所寫的 Common LISP: the Language ,該書是在 1984. 由 Digital Press 出版社所出版,它的第一版很容易讀,第二版則描述了更多最新的標準。(對於一般的程序設計師而言,第一、二版關於最新標準的些微差異並不會有任何影響。)
另外還有一本由 Dave Touretsky 所寫的書也有很多人跟我推薦,不過由於我並沒有去讀過,所以我也無法評論。
[編輯] 符號
符號(Symbols)就是一串字元。你可以在符號中包含字母、數字、連接符等等,唯一的限制就 是要以字母開頭。(如果你只輸入數字,最多再以一個連接符開頭的話,LISP會認 為你輸入了一個整數而不是符號。)下面是符號的一些範例:
a b c1 foo bar baaz-quux-garply
你可以像下面的例子一樣的使用符號 。 (在 > 提示符號後面的就是你的輸入給 Lisp 解釋器的內容,而其它的就是 Lisp 解釋器所回顯的結果。而 ";" 分號則是 Lisp 的註釋符號,在分號之後到該行結束的數據都會被解釋器忽略。)
> (setq a 5) ; 把数值 5 存入 a 这个符号里面。 5 > a ; 取得 a 这个符号所存的值。 5 > (let ((a 6)) a) ; 暂时性地把 a 这个符号的值给设定成 6 6 > a ; 当脱离 let 区块之后, a 的值又变回到 5 5 > (+ a 6) ; 把 a 这个符号的值当作是加法函数的参数 11 > b ; 尝试着取得并没有值的 b 这个符号的值看会发生什么事情? Error: Attempt to take the value of the unbound symbol B
有兩個比較特別的符號就是 t 跟 nil 。t 這個符號所定義的值就是 t ,而 nil 這個符號所定義的值就是 nil 。 Lisp 分把把 t 跟 nil 這兩個值拿來表示「真」與「假」。一個最典型會用的 t 跟 nil 的例子就是 if 函數,將會更清楚的解釋介紹 if 函數。
> (if t 5 6) 5 > (if nil 5 6) 6 > (if 4 5 6) 5
最後一個例子或許會讓你感到很奇怪,不過它並沒有錯誤。原因是 nil 表示「假」,而任何其它的值都表示「真」。(除非你有理由要這樣寫程序,不然通常我們還是習慣用 t 來表示「真」,這樣讀程序的時候也比較清楚。)
像 t 和 nil 這樣的符號被稱為自解析符號,因為他們解析為自身。實際上,還有一大類的自解析符號稱為 關鍵字;任一以冒號開頭的符號都是關鍵字。(下面是一些關鍵字的應用)如下 所示:
> :this-is-a-keyword :THIS-IS-A-KEYWORD > :so-is-this :SO-IS-THIS > :me-too :ME-TOO
[編輯] 數值
整數的定義就是一連串的數字,並且最前面可以選擇性的加上+ 或 - 。而實數包含有整數,而且比整數定義廣的是,實數還可以有小數點,也可以用科學記號表示。有理數則是兩個整數相除而得,也就是在兩個整數中間加上 / 。 Lisp 還支持複數類型,利用像是 #c(r i) 這樣表示複數,其中 r 表示複數的實部,i 表示複數的虛部。上列的任何一種都稱做是數值(Number)類型。
下面是一些數值類型的例子:
5 17 -34 +6 3.1415 1.722e-15 #c(1.722e-15 0.75)
對於數值可以進行運算,一些常見的數值函數如 +, -, *, /, floor,ceiling, mod, sin, cos, tan, sqrt, exp, expt 都有內建,而且這些內建的數值函數可以接受任何數值類型的參數。 +, -, *, / 這四個函數的返回值類型會隨輸入參數的類型而自動延伸為較廣的類型範圍,比如說整數加上有理數的返回值,就會是範圍較廣的有理數;而有理數加上實數的返回值,是實數;實數加上複數的返回值,則是複數。下面是一些例子:
> (+ 3 3/4) ;返回值类型范围自动加广 15/4 > (exp 1) ;自然对数的基底 e 2.7182817 > (exp 3) ;e*e*e 20.085537 > (expt 3 4.2) ;指数函数,以 3 为底数,次方数是 4.2 100.90418 > (+ 5 6 7 (* 8 9 10)) ;内建的 +,-,*,/ 四个算数函数都可以接受多个参数值的调用
對於整數絕對值並沒有任何參數大小的限制,完全取決於執行時使用的計算機內存夠不夠。但是要注意,對於大數的計算,越大的數值計算機執行效率一定越慢。(有理數的計算也是會比較慢,尤其是拿來跟不是很大的整數,還有小數的計算速度相比較,更明顯。)
[編輯] 點對
點對(cons,複數形式conses)就是一個有兩個欄位的數據紀錄。由於一些歷史上的因素,這兩個欄位分別稱作 "car" 跟 "cdr" 。(在第一台實作 Lisp 語言的機器上, CAR 與 CDR 指令分別表示"Contents of Address Register" 及"Contents of Decrement Register"。而 cons 就是透過這兩個緩存器而實作的。) Cons 很容易使用:
> (cons 4 5) ; 设置一个 cons ,其中 car 设为数字 4 ,而 cdr 设为数字 5 。 (4 . 5) > (cons (cons 4 5) 6) ; 设置一个 cons ,其中 car 设为一个点对(4 . 5),而 cdr 设为数字 5 。 ((4 . 5) . 6) > (car (cons 4 5)) ; 取出 (4 . 5) 的 car 设定值。 4 > (cdr (cons 4 5)) ; 取出 (4 . 5) 的 cdr 设定值。 5
[編輯] 鏈表
利用點對(Cons)我們可以創造出很多結構,而當中最簡單的,或許就是鏈表(linked list)。鏈表其實就是把 Cons 的 CAR 指定成某些元素,而把 CDR 指定到另一個 Cons 或是 NIL 。如下,我們可以經由 list 函數來創造鏈表。
> (list 4 5 6) (4 5 6)
看到上面的例子,你應該有注意到 Lisp 在列印鏈表的時候,會有一些原則:它輸出的時候會省略掉一些 . 連結點對的點,以及 () 括弧。而省略的原則如下,如果這個點對的 CDR 是 NIL 的話,那這個 NIL 跟它前面的連結點將不會被印出來;如果這個點對 A 的 CDR 是另外一個點對 B 的話,那在點對 B 前面的連結點以及點對 B 本身的小括弧都不會被印出來。如下例子:
> (cons 4 nil) (4) > (cons 4 (cons 5 6)) (4 5 . 6) > (cons 4 (cons 5 (cons 6 nil))) (4 5 6)
最後的這個例子,其實跟直接調用函數 (list 4 5 6) 是等價的。注意 NIL 在這兒的含義就是沒有包含任何元素的鏈表。比如說,包含兩個元素的鏈表(a b)中,cdr是(b),一個含有單個元素的鏈表;包含 一個元素的鏈表(b),cdr是nil,故此這裡必然是一個沒有元素的鏈表。
NIL 的 CAR 跟 CDR 都定義成 NIL 。
如果我們把鏈表指給任何變數,那就可以如下當成堆棧(stack)來使用:
> (setq a nil) NIL > (push 4 a) (4) > (push 5 a) (5 4) > (pop a) 5 > a (4) > (pop a) 4 > (pop a) NIL > a NIL
[編輯] 函數
之前我們看過函數(Functions)的例子了。下面是其它函數的例子:
> (+ 3 4 5 6) ; 加法函数可以接受任意多的输入参数
18
> (+ (+ 3 4) (+ (+ 4 5) 6)) ; 或是你也可以像这样加,哈~
22
> (defun foo (x y) (+ x y 5)) ; 定义一个叫做 foo 的函数
FOO
> (foo 5 0) ; 调用函数,传入的参数个别是 5 跟 0
10
> (defun fact (x) ; 以递归调用的方式定义函数 fact
(if (> x 0)
(* x (fact (- x 1)))
1))
FACT
> (fact 5)
120
> (defun a (x) (if (= x 0) t (b (- x)))) ; 以两个函数相互调用的递归方式来定义函数
A
> (defun b (x) (if (> x 0) (a (- x 1)) (a (+ x 1))))
B
> (a 5)
T
> (defun bar (x) ; 一个函数的定义里面如果有很多语句句的话
(setq x (* x 3)) ; 那整个函数的返回值,
(setq x (/ x 2)) ; 将会是最后的一个语句句
(+ x 4))
BAR
> (bar 6)
13
當初我們在定義 foo 函數的時候,要求要兩個傳入值 x 及 y 。所以每當要調用 foo 時,都要恰好給它兩個傳入值,第一個傳入值將會變成在 foo 函數裡面的 x 變數值,而第二個傳入值將會變成在 foo 函數裡面的 y 變數值。而在 Lisp 裡面,大多數變數其實都是 lexically soped ,也就是說,如果 foo 的定義裡面有調用 bar 函數,在 bar 函數被調用的時候,依然看不到 foo裡面 x 變數的值滴。這種指定變數的值所存在的可視範圍,稱做是綁定(binding)。在函數定義的時候,其實有些傳入值可以當作是選用的/非必需的。
任何傳入值只要在前方加上 &optional 就會變成是選用的/非必需的。
如下例子:
> (defun bar (x &optional y) (if y x 0)) BAR > (defun baaz (&optional (x 3) (z 10)) (+ x z)) BAAZ > (bar 5) 0 > (bar 5 t) 5 > (baaz 5) 15 > (baaz 5 6) 11 > (baaz) 13
你可以使用一或二個參數調用 bar 函數。如果你只有用一個參數調用 bar 函數,那個參數就會設定給 x ,而 y 的默認值則會是 NIL ;如果你使用兩個參數調用bar 函數,那 x 跟 y 就分別被設定成第一及第二個傳入參數。而 baaz 函數有兩個選用參數,並且這兩個參數都有默認值,如果 baaz 時,只有給它一個傳入參數,則 z 的直就會是默認值 10 ,而非 NIL ;而如果調用baaz 函數時,沒有給任何傳入值,則 x 跟 z 的值就會是默認值 3 跟 10 。你可以讓你設計的函數接受任意多個的輸入參數,以要在參數列加上一個 &rest的參數就可以了, Lisp 將會把所有沒有被指到到變數名稱的參數搜集再起變成一個鏈表,變且把這個鏈表指定給 &rest 的參數。如下:
> (defun foo (x &rest y) y) FOO > (foo 3) NIL > (foo 4 5 6) (5 6)
最後,你還可以將你的函數設計另一種輸入選用參數的方式,就是透過關鍵字(keyword)參數。這種方式的傳入參數沒有前後次序性,因為輸入參數的時候都要指定關鍵字參數的名稱。
> (defun foo (&key x y) (cons x y)) FOO > (foo :x 5 :y 3) (5 . 3) > (foo :y 3 :x 5) (5 . 3) > (foo :y 3) (NIL . 3) > (foo) (NIL)
就算是利用 &key 設定的 keyword 參數,也可以有默認值,如下範例:
> (defun foo (&key (x 5)) x) FOO > (foo :x 7) 7 > (foo) 5
[編輯] 顯示
顯示(Printing)。有些函數會導致輸出,而最簡單的輸出,就是透過調用 print 函數,它會把參數給輸出到屏幕上,然後函數的返回值也是剛剛輸出的結果。
> (print 3) 3 3
第一個 3 是因為調用 print 函數而把參數輸出到屏幕上,第二個 3 則是調用函數之後的返回值。如果你希望輸出結果複雜一點,你可以使用 format 函數。見下面範例:
> (format t "An atom: ~S~%and a list: ~S~%and an integer: ~D~%"
nil (list 5) 6)
An atom: NIL
and a list: (5)
and an integer: 6
在調用 format 函數的時候,第一個參數只可以 T, NIL, 或是其它的輸出串流。其 T 表示要輸出到終端屏幕上, NIL 表示不要輸出任何值,而使要把原本要輸出的字元串當作是函數的返回值回傳。而如果是其它的輸出鏈表,則可以指定是任何像是文件、終端、其它程序都可以。此教學講義不會對其他的輸出串流提供更多的解釋,言謹於此。第二個輸入的參數則是一個格式化的樣板,也就是一個字元串,字元串裡面可能含有一些的格式化指令。
其它剩下的參數,則是跟之前字元串裡面的格式化指令是相對應的,Lisp 將會把剩下的參數用來代換至字元串裡面相對應的格式化指令。 Lisp 會根據格式化指令的適當屬性,把其餘參數用適當的方式帶換掉之後,在輸出格式化之後的字元串。
Format 函數的返回值預設會是 NIL ,除非在調用 Format 函數的第一個參數是 NIL ,如此,則不會把格式化之後的字元串輸出到任何對象,而是會把格式化之後的字元串當作是函數調用的返回值。
在上面範例裡面用到的三個不同的格式化指令:~S, ~D 跟 ~% 。第一個 ~S 會接受任何的Lisp 對象,並且會用該對象的可以顯示的方式來取代掉 ~S (當中可以顯示的方式跟直接利用 print 函數輸出該對象是一樣的方式)。第二個 ~D 會接受任何的整數值。第三個 ~% 則不會被任何之後的輸入參數所取代,可是它會自動轉換成換行的指令。
另外還有一個有用的格式化指令是 ~~ ,它會自動輸出成只有一個 ~ 。如果還要更多、更多額外的格式化指令,可以參考其它的 Lisp 手冊。
[編輯] 窗體與頂層循環
你一行行打字,所輸入給 Lisp 解釋器的那些數據就稱做是窗體(forms) ,Lisp 解釋器會一直讀取你給它的窗體,然後進行運算/評估,並且把返回值顯示出來,這個一再重複的過程就稱作是個「讀取—評估—顯示」的循環。 有些窗體可能會導致錯誤(也就是程序代碼沒寫好啦),當執行程序的時候發生錯誤的話, Lisp 會把進入調試狀態,以便讓我們找出錯誤發生的原因。每個 Lisp 版本的調試模式都不太一樣,但是至少當我們對大多數的調試程序輸入 "help" 或是 ":help" ,它應該會顯示相關輔助說明文字。
一般而言,窗體里的數據要麼就是無法再細分的原子(atom),像是字元、整數、字元串....,這些都是屬於無法再細分的原子,要麼窗體里的數據就是一個鏈表。如果窗體的數據是原子,那 Lisp 通常很快就可以評估出它的返回值,字元返回值就是它所表示的值,整數跟字元串的返回值就是它們本身而已。但如果窗體的數據是一個鏈表,那 Lisp 會把這個鏈表的第一個元素當作是函數的名稱,把其它元素評估完之後的值當作是輸入參數,然後把這整個鏈表當作是函數調用,舉例來說,如果窗體的數據是 (+ 3 4) ,Lisp 會把 + 當作是最後要調用的函數名稱,然後它逐步評估求值,3 評估值(運算)之後是返回值是 3 ,4 評估值(運算)之後返回值是 4 ,而後調用 + 這個函數,而傳入 + 這個函數的參數則是剛剛已經評估完的值 3 跟 4 ,因此調用完 + 這個函數的返回值會是 7 ,最後 Lisp 再把它顯示給我們看。
譯註:而我們在使用 Lisp 解釋器的時候,位在 > 之後要給它的,就是位在頂層的「讀取—評估—顯示」的循環。
位在頂層的「讀取—評估—顯示」的循環,其實有其它的好處,其中一個好處就是可以隨時取出之前運算的窗體數據,Lisp 用 *, **, 跟 *** 分別表示在此窗體的前一、二、三個評估值的窗體。如下例:
> 3 ; 要评估的窗体是 3 ,所以返回值是 3 3 > 4 ; 要评估的窗体是 4 ,所以返回值是 4 > 5 ; 要评估的窗体是 5 ,所以返回值是 5 5 > *** ; 要评估的窗体是,在这之前推三步的那个窗体,所以评估 3 之后,返回值是 3 3 > *** ; 要评估的窗体是,在这之前推三步的那个窗体,所以评估 4 之后,返回值是 4 4 > *** ; 要评估的窗体是,在这之前推三步的那个窗体,所以评估 5 之后,返回值是 5 5 > ** ; 要评估的窗体是,在这之前推两步的那个窗体,所以评估 4 之后,返回值是 4 4 > * ; 要评估的窗体是,在这之前推一步的那个窗体,所以评估 4 之后,返回值是 4 4
[編輯] 特殊窗體
有一些比較特殊的輸入窗體(Special forms)看起來就像是函數調用,可是實際上卻不是函數調用。這些特殊窗體包含有流程式控制制命令,如 if 和 do loop 語句等,以及用來設定變數的命令,如 setq, setf, push, 與 pop ,還有用來定義的命令,如定義函數的 defun 及定義結構的 defstruct ,還有用來綁定的命令,如 let 。(當然上面並沒有提及所有的特殊窗體,往下看,還會繼續介紹其它的特殊窗體。)
最特別的有一個特殊窗體 quote 是用來避免它的輸入參數進入評估值的步驟,也就是它會讓輸入參數以原來的形式當作是返回值,並不會先經過評估值的步驟。舉例如下:
> (setq a 3) 3 > a 3 > (quote a) A > 'a ; 'a 是 (quote a) 的缩写 A
另外還有一個類似的特殊窗體就是 function 窗體,function 會讓它的輸入參數被視作是某個函數,而不是被拿來評估值。 舉例如下:
> (setq + 3) 3 > + 3 > '+ + > (function +) #<Function + @ #x-fbef9de> > #'+ ;#'+ 是 (function +) 的缩写 #<Function + @ #x-fbef9de>
function 這個特殊窗體常常被用在要把函數當作是參數來傳遞的時候。本文後面會繼續介紹到一些例子,就是把函數拿來當作是輸入參數,此時就會需要用到 function 這個特殊窗體。
[編輯] 綁定
綁定(Binding)是 lexically scoped 的變數值設定。它發生在當函數調用時候,參數列的變數是用綁定的方式設定變數值:在函數調用期間,此時此函數定義時的參數列,其值被綁定在函數調用發生時的輸入參數。其實不管在哪程序裡面的哪裡,你也可以利用 let 這個特殊窗體來綁定變數值,其使用形式如下:
(let ((var1 val1)
(var2 val2)
...)
body)
Let 把 var1 綁定成 val1 ,把 var2 綁定成 val2 ,如此類推,然後它會執行 body 這一區塊的程序語句。 上面 Let 特殊窗體裡面的 body 程序區塊語句執行的結果就會像是在函數調用時的程序語句有一樣的效果。如下範例:(譯註:這像是函數調用,只是把參數列改成 let 特殊窗體而已,而 body 執行完之後的返回值,就會是函數返回值。)
> (let ((a 3)) (+ a 1)) ; 在 let 窗体里面,绑定 a 为 3,然后执行 a+1 ,返回值就是 4 。
4
> (let ((a 2)
(b 3)
(c 0))
(setq c (+ a b))
c)
5
> (setq c 4)
4
> (let ((c 5)) c)
5
> c
4
如果有綁定值是 NIL 的,如 (let ((a nil) (b nil)) ...) ,就可以縮寫成 (let (a b) ...) 。 Let 特殊窗體裡面的綁定值 val1, val2 ... 等的值不能參照 var1, var2 ... 等,因為綁定正在發生,還沒有結束。如下範例:
> (let ((x 1)
(y (+ x 1)))
y)
Error: Attempt to take the value of the unbound symbol X
如果變數 x 在上面這段程序執行之前已經有全域變數值,那就會發生很莫名奇妙的結果,如下範例:
> (setq x 7)
7
> (let ((x 1)
(y (+ x 1)))
y)
8
還有一個 let* 也是特殊窗體,它跟 let 很像,但是不同的地方是 let* 可以允許綁定值參考之前已經綁定的變數。如下範例:
> (setq x 7)
7
> (let* ((x 1)
(y (+ x 1)))
y)
2
下面這樣的窗體
(let* ((x a)
(y b))
...)
其實就等同於,如下
(let ((x a))
(let ((y b))
...))
[編輯] 動態作用域
let 跟 let* 這樣的特殊窗體提供了 lexical scoping ,那就像你在寫 C 或是 Pascal 程序 所預期一樣的變數可視範圍。而還有一種動態作用域(Dynamic scoping)就如同 BASIC 語言所提供的一樣,如果你指定一個變數值給動態作用域的變數,那不管你在何時去讀取變數的值,都會是一開始指定的那個變數值,除非你有給它另一個新變數值,以取代之。
在 Lisp 裡面,這些動態作用域的變數被稱做是特殊變數(special variables),你可以透過 defvar 特殊窗體來定義特殊變數。下面是一些 lexically 跟 dynamically scoped 變數的例子。
在下面的這麼範例裡面, check-regular 函數裡面調用了 regular 這個一般變數(亦即lexically scoped 的變數)。因為 check-regular 函數的定義是在 let 區塊之外,所以 let區塊裡面的 regular 綁定並不會影響到 check- regular 函數裡面 regular 變數值,所以check-regular 的返回值是 regular 變數的全域可視範圍的值。
> (setq regular 5) 5 > (defun check-regular () regular) CHECK-REGULAR > (check-regular) 5 > (let ((regular 6)) (check-regular)) 5
在下面的這麼範例裡面, check-special 函數裡面調用了 special 這個特殊變數(亦即dynamically scoped 的變數)。因為在 let 區塊裡面有一段暫時調用了 check-special 這個函數,而且 let 有暫時綁定 special 特殊變數新值,所以 check-special 會返回的是受到let 區塊綁定影響的區域變數值。
> (defvar *special* 5) *SPECIAL* > (defun check-special () *special*) CHECK-SPECIAL > (check-special) 5 > (let ((*special* 6)) (check-special)) 6
為了方便記億與區別,通常會把特殊變數的名稱前後會用 * 包圍起來。特殊變數主要被用在當作是全域變數,因為程序設計師通常會預期區域變數是 lexical scoping ,而全域變數是 dynamic scoping 。
如果需要更多關於 lexical scoping 跟 dynamic scoping 的區別,請參看_Common LISP: the Language_ 這本書。
[編輯] 數組
函數 make-array 可以產生數組(Arrays),而函數 aref 則可以存取數組裡面的元素。數組裡所有元素的初始則設定值是 NIL 。如下範例:
> (make-array '(3 3)) #2a((NIL NIL NIL) (NIL NIL NIL) (NIL NIL NIL)) > (aref * 1 1) NIL > (make-array 4) ; 一维数组的维度不需要额外的小括号 #(NIL NIL NIL NIL)
數組的索引值必定是由 0 開始起算。
繼續往下看,將會學到如何設定數組的元素。
[編輯] 字元串
所謂的字元串(Strings)就是被兩個 " 所包夾在中間的字元序列。Lisp 實際上是把字元串視為是可變長度的字元數組。如果要表示的字元串裡面本身就包含有 " 的話,那需要在 "前面加上倒斜線 \ ,而用連續的兩個倒斜線來是表示字元串裡面的一個倒斜線。 如下範例:
"abcd" 包含有 4 个字符
"\"" 包含有 1 个字符
"\\" 包含有 1 个字符
下面是一些用來處理字元串的函數範例:
> (concatenate 'string "abcd" "efg") ; 连接字符串用 concatenate 函数 "abcdefg" > (char "abc" 1) #\b ; Lisp 会在字符前面加上 #\ 用来表示字符。 > (aref "abc" 1) #\b ; 请记住,字符串其实就是字符数组而已。
連接字元串用的 concatenate 函數實際上可以用來連接任何類型的序列:
> (concatenate 'string '(#\a #\b) '(#\c)) "abc" > (concatenate 'list "abc" "de") (#\a #\b #\c #\d #\e) > (concatenate 'vector '#(3 3 3) '#(3 3 3)) #(3 3 3 3 3 3)
[編輯] 結構
Lisp 的結構(Structures)就類似 C 語言的 struct 跟 Pascal 語言的 record 。下面是一個範例:
> (defstruct foo
bar
baaz
quux)
FOO
這個範例定義了一個名為 foo 的數據類型,這個類型的結構實際上包含了三個欄位。在定義結構的同時,實際上它也定義了四個可以操作這個數據類型的的函數,分別是make-foo, foo-bar, foo-baaz, 跟 foo-quux 。第一個函數 make-foo 可以用來產生 foo 數據類型的對象,而其它三個函數則可以用來取得 foo 數據類型當中對應的數據域位。下面是,如何使用這些函數的範例:
> (make-foo) #s(FOO :BAR NIL :BAAZ NIL :QUUX NIL) > (make-foo :baaz 3) #s(FOO :BAR NIL :BAAZ 3 :QUUX NIL) > (foo-bar *) NIL > (foo-baaz **) 3
只要是 foo 結構所有的欄位,在產生對象時候用的 make-foo 函數都可以接受對應欄位的keyword 參數。而存取數據域位的取用函數則可以接受一個 foo 對象當作是輸入參數,並且返回該結構里對應的數據域位之值。
繼續往下看,將會學到如何設定結構里各欄位的值。
[編輯] Setf
在 Lisp 裡面有某些窗體實際上表示的就是內存里的位置,舉例來說,如果 x 是foo 數據類型的結構的話,那 (foo-bar x) 表示的就是 x 裡面的 bar 數據域位。 另外,如果 y 是一維數組,那 (aref y 2) 表示的就是 y 數組裡面的第三個元素。
而 setf 特殊窗體可以接受兩個參數,第一個參數是一個內存里的位置,而第二個參數在被評估求值之後,所評估出來的值將會被存入第一個參數所指的內存位置。舉例如下:
> (setq a (make-array 3)) #(NIL NIL NIL) > (aref a 1) NIL > (setf (aref a 1) 3) 3 > a #(NIL 3 NIL) > (aref a 1) 3 > (defstruct foo bar) FOO > (setq a (make-foo)) #s(FOO :BAR NIL) > (foo-bar a) NIL > (setf (foo-bar a) 3) 3 > a #s(FOO :BAR 3) > (foo-bar a) 3
Setf 是唯一可以用來設定結構里數據域位的值,以及設定數組裡元素之值的方法。
下面是跟 setf 及相關的函數調用的一些範例:
> (setf a (make-array 1)) ; setf 作用在单一个变量上面的效果跟 setq 一样。 #(NIL) > (push 5 (aref a 1)) ; push 也可以拿来当作是 setf 使用(不过参数顺序不太一样喔!) (5) > (pop (aref a 1)) ; 既然 push 可以存值,那 pop 当然就可以取值。 5 > (setf (aref a 1) 5) 5 > (incf (aref a 1)) ; incf 的功用是从内存位置读取出值,然后累加 6 ; 最后在把累加完之后的值,存回到相同的内存位置。 > (aref a 1) 6
[編輯] 布爾值與判斷條件
Lisp 使用其值為本身的 NIL 表示「假」。任何其它不是 NIL 的值都表示真。 然而除非有特殊理由要這樣處理,不然我們還是會習慣上利用其值為本身的 T表示「真」。
Lisp 提供了一系列的標準的邏輯函數,比如像是 and, or 以及 not 函數。and 以及 or 函數是屬於 short-circuit ,也就是說,如果 and 函數的有任何一個個參數的運算結果已經是 NIL ,拿之後的參數將不用進行運算估值;而 or函數如果有任何一個參數運算結果事 T ,那之後的參數就不會進行運算估值。
Lisp 也提供了幾個特殊窗體用來做控制判斷執行的條件。最簡單的就是 if 語句,在 if 語句的第一個參數將會決定,接下來執行的會是第二個或是第三個參數。
> (if t 5 6) 5 > (if nil 5 6) 6 > (if 4 5 6) 5
如果你在 if 語句之後的 then(第二個參數) 或是 else(第三個參數) 的部分想要執行超過一個以上的語句,那你可以使用 progn 這個特殊窗體。 progn 將會執行在它內部的每一個局域,並且返回最後一個評估值之後的結果。
> (setq a 7)
7
> (setq b 0)
0
> (setq c 5)
5
> (if (> a 5)
(progn
(setq a (+ b 7))
(setq b (+ c 8)))
(setq b 4))
13
if 語句如果缺乏 then(第二個參數) 或是 else(第三個參數) 的部分,其實也可以用 when 或是 unless 特殊窗體改寫,如下範例:
> (when t 3) 3 > (when nil 3) NIL > (unless t 3) NIL > (unless nil 3) 3
when 跟 unless 特殊窗體並不像 if 只可以放一個語句,他們可以放任一個數的語句在他們內部當作參數。(例如: (when x a b c) 就等價于 (if x (progn a b c)) 。 )
> (when t
(setq a 5)
(+ a 6))
11
更複雜的控制判斷條件可以透過 cond 特殊窗體來處里, cond 特殊窗體相當於if ... else if ... fi 控制判斷條件一樣。
cond 特殊窗體包含有開頭的 cond 字元,後面接的一連串的判斷子句,每一個判斷句都是一個鏈表,該鏈表的第一個元素就是判斷條件,而剩下的元素(如果有的話)就是要有可能要執行的語句。 cond 特殊窗體會找尋第一個滿足判斷條件為真(也就是,不是 NIL)的子句,然後執行該子句裡面對應的語句,並且把運算評估完的結果當作是返回值。而剩下的其它子句就不會被運行評估了, cond 特殊窗體只會運行至多一個符合判斷結果為真的子句語句。如下範例:
> (setq a 3) 3 > (cond ((evenp a) a) ;如果(if) a 是偶数,则返回值为 a ((> a 7) (/ a 2)) ;不然,如果(else if) a 比 7 大,则返回值为 a/2 ((< a 5) (- a 1)) ;不然,如果(else if) a 比 5 小,则返回值为 a-1 (t 17)) ;不然(else),返回值为 17 2
如果在 cond 特殊窗體裡面,判斷條件為真且要執行的那個子句,並沒有要執行的語句部分的話,那 cond 窗體就會返回判斷條件為真的那個結果。如下:
> (cond ((+ 3 4))) 7
接下來是一個用到 cond 特殊窗體的遞歸函數定義的巧妙小例子。你或許可以試著證明看任何 x 以比 1 大的整數值帶入,最後這個遞歸函數都會終止。(如果你成功證明出來了,請務必要昭告天下!) (譯註:這是數學界有名的 3x+1 猜想,至 2006 年目前依然無人成功證出。)
> (defun hotpo (x steps) ; hotpo 会把偶数减半,把奇数乘三后加一
(cond
((= x 1) steps)
((oddp x) (hotpo (+ 1 (* x 3)) (+ 1 steps)))
(t (hotpo (/ x 2) (+ 1 steps)))))
A
> (hotpo 7 0) ; 从 7 经 hotpo 运算到 1 共要经过 16 步。
16
Lisp 也有一个 case 语句句,就类似 C 语言的 switch 语句句一样。如下范例:
> (setq x 'b)
B
> (case x
(a 5) ; 如果 x 是 a ,那返回值就是 5
((d e) 7) ; 如果 x 是 d 或 e ,那返回值就是 7
((b f) 3) ; 如果 x 是 b 或 f ,那返回值就是 3
(otherwise 9)) ; 此外,那返回值就是 9
3
最後的 otherwise 子句,所表示的意思是"如果 x 不是 a, b, d, e, 或是 f ,那返回值就是 9 。″
[編輯] 迭代結構
在 Lisp 中最簡單的迭代結構(Iteration)就是 loop(循環) 了: loop 結構會一再重複執行其內部的指令,直到執行到 return 特殊窗體才會結束。如下範例:
> (setq a 4) 4 > (loop (setq a (+ a 1)) (when (> a 7) (return a))) 8 > (loop (setq a (- a 1)) (when (< a 3) (return))) NIL
下一個最簡單的迭代結構就是 dolist : dolist 會把變數依序綁值于鏈表裡面的所有元素,直到把達到鏈表底部沒有元素才結束。如下範例:
> (dolist (x '(a b c)) (print x)) A B C NIL
Dolist 的返回值必定是 NIL 。請注意看上面範例裡面 x 綁訂的值卻從未是 NIL ,在 C 後面的 NIL 是 dolist 的返回值,也就是要滿足 "讀取—評估—顯示″循環必定會顯示的評估(運算)值。
最複雜的迭代結構主要就是 do 循環了。一個 do 循環的範例看起來就像下面這樣:
> (do ((x 1 (+ x 1))
(y 1 (* y 2)))
((> x 5) y)
(print y)
(print 'working))
1
WORKING
2
WORKING
4
WORKING
8
WORKING
16
WORKING
32
在上面範例裡面,在 do 循環的後面的第一個大區塊里的是變數名稱,以及該變數綁定的初始值,還有每次循環運行一圈之後,變數的更新條件。第二個大區塊里的則是 do 循環的終止條件,以及 do 循環結束之後的返回值。(譯註:此終止條件是在每次進入循環主體前檢查,也就是循環主體可能會連一次都沒有被執行到。)最後一個大區塊,則是循環主體。do 窗體會先如同 let 特殊窗體依樣綁定變數初始值,然後檢查循環終止條件是否成立,只要每次檢查終止條件不成立,那就會執行循環主體,然後再回到檢查終止條件地部分,直到檢查到終止條件成立,則返回當初在第二大區塊的所指定的返回值。
另外還有一個 do* 窗體,功能如同上面的 do 窗體,只是相對於把上面語句的 let 改成let* 而已。
[編輯] 無定位返回
前一節中迭代示例里的return語句是一個無定位返回(Non-local Exits)的示例,另一個是 return-from,它從包圍它的函數中返回指定值。
> (defun foo (x) (return-from foo 3) x) FOO > (foo 17) 3
實際上,return-from 語句可以從任何已命名的語句塊中退出──只是默認情況下函數是唯一的命名語句塊而已。我們可以用 block語句自己定義一個命名語句塊。
> (block foo (return-from foo 7) 3) 7
return 語句可以從任何nil命名的語句塊中返回。默認情況下循環是nil命名,而我們可以創建自己的nil標記語句塊。
> (block nil (return 7) 3) 7
另外一個無定位退出語句是 error 語句:
> (error "This is an error") Error: This is an error The error form applies format to its arguments, then places you in the debugger.
error語句格式化它的參數,然後進入調試器。
[編輯] Funcall,Apply與Mapcar
在本文前半塊,我曾說過要給幾個把函數名稱當作是函數傳入參數的例子。舉例如下:
> (funcall #'+ 3 4) 7 > (apply #'+ 3 4 '(3 4)) 14 > (mapcar #'not '(t nil t nil t nil)) (NIL T NIL T NIL T)
funcall 會調用以第一個參數為名的函數,並把 funcall 的其它參數當作是要調用的函數的傳入參數。
apply 就像是 funcall 一樣的功用,除了 apply 的最後一個參數必須要是鏈表;這最後鏈表裡面的元素,就像是在使用 funcall 時的額外參數一樣。
mapcar 的第一個參數必須是可以作用於單一傳入值的函數名稱, mapcar 會把該函數名稱套用在,其後參數鏈表的每一個元素上,並且把函數調用結果集合起來,形成新的鏈表回傳。
funcall 跟 apply 就是因為他們的第一個參數可以是變數,所以特別有用。舉例應用如,當一個搜索引擎可以採用啟髮式的函數當作是參數,並且利用 funcall 或 apply 把那個函數參數作用在狀態語句上。稍後會介紹的排序函數,也是利用 funcall 來傳遞排序時要用的哪個比較函數來比較大小。
mapcar 跟匿名函數(後面會介紹)一起使用,可以取代很多循環的使用。
[編輯] 匿名函數
如果你想要創造一個暫時性使用的函數,並且不想煩惱應該給那個函數什麼名稱,此時就可以使用匿名函數(lambda)。
> #'(lambda (x) (+ x 3)) (LAMBDA (X) (+ X 3)) > (funcall * 5) ; 译注: * 表示前一个输入窗体,在此就是 #'(lambda (x) (+ x 3)) 8
把 lambda 跟 mapcar 一起組合使用可以取代大多數的循環。如下範例,下面的兩個窗體是等價的。
> (do ((x '(1 2 3 4 5) (cdr x))
(y nil))
((null x) (reverse y))
(push (+ (car x) 2) y))
(3 4 5 6 7)
> (mapcar #'(lambda (x) (+ x 2)) '(1 2 3 4 5))
(3 4 5 6 7)
[編輯] 排序
Lisp 提供了兩個主要的排序(Sorting)函數: sort 跟 stable-sort 。
> (sort '(2 1 5 4 6) #'<) (1 2 4 5 6) > (sort '(2 1 5 4 6) #'>) (6 5 4 2 1)
sort 的第一個參數是一個鏈表,而第二個參數則是一個比較大小用的比較函數的名稱。sort 函數並不保證排序的穩定性,也就是說,如果有兩個元素 a 與 b 滿足(and (not (< a b)) (not (< b a))) ,sort 或許有可能在排序之後,會對調 a 與 b 的順序。而 stable-sort 跟 sort 使用方式完全一樣,除了 stable-sort 保證對於相同的元素必定不會對調順序。
請務必注意: sort 允許破壞他的輸入參數序列,所以如果原始傳入參數對你而言是很重要的,請先利用 copy-list 或 copy-seq 做好備份。
[編輯] 相等
Lisp 對於「相等(Equality)」的意義有很多種類型。 數值上的相等是用 = 來判別。兩個字元則是用 eq 來檢查他們是否是同一個。兩個有相同值的鏈表拷貝並不是 eq 的(譯註:不同的內存位置),但這兩個有相同值的鏈表拷貝卻是 equal 的(譯註:儲存的數據是一樣的)。
> (eq 'a 'a) T > (eq 'a 'b) NIL > (= 3 4) NIL > (eq '(a b c) '(a b c)) NIL > (equal '(a b c) '(a b c)) T > (eql 'a 'a) T > (eql 3 3) T
eql 判斷式等價于 "判斷是否是相同類型" 加上 "如果同是字元,判斷是否 eq " 再加上"如果同是數值,判斷是否 = "的合體。
> (eql 2.0 2) NIL > (= 2.0 2) T > (eq 12345678901234567890 12345678901234567890) NIL > (= 12345678901234567890 12345678901234567890) T > (eql 12345678901234567890 12345678901234567890) T
用在 字元跟數值上, equal 判斷式就等價于 eql 。對於兩個 cons 而言,如果他們的 car跟 cdr 都是 equal ,那這兩個 cons 就是 equal 的。對於兩個 structures (結構) 而言,如果他們有相同的數據類型,並且相對應的數據域位是 equal 的,那這兩個結構就是 equal 的。
[編輯] 一些有用的鏈表函數
下面是一些用來操作鏈表的有用函數。
> (append '(1 2 3) '(4 5 6)) ; 连结许多链表
(1 2 3 4 5 6)
> (reverse '(1 2 3)) ; 反转一个链表里面的元素
(3 2 1)
> (member 'a '(b d a c)) ; 集合元素的"属于"判断 -- 它会返回第一个找到的元素
(A C) ; 至后方所有元素所形成的链表,也就是找第一个 car 是该元素的链表
; 译注:空链表NIL 即为假,其它任何非空链表皆表示真。
> (find 'a '(b d a c)) ; 另一个检查元素是否属于该集合的方法就是用 find 。
A
> (find '(a b) '((a d) (a d e) (a b d e) ()) :test #'subsetp)
(A B D E) ; find 是很有弹型的,可以传入要用来判断的函数。
; 上面例子就是改用 subsectp (检查是否为子集合) 来找寻满足条件的集合。
> (subsetp '(a b) '(a d e)) ; 检查是否为子集合
NIL
> (intersection '(a b c) '(b)) ; 求集合的交集
(B)
> (union '(a) '(b)) ; 求集合的联集
(A B)
> (set-difference '(a b) '(a)) ; 求差集合
(B)
Subsetp, intersection, union, 和 set- difference 都有一個基本假設就是傳入值的參數鏈表內不會有重複的元素(也就是集合),不然的話,像是 (subsetp '(a a) '(a b b)) 判斷出來的返回值就可能是假。
Find, subsetp, intersection, union, 和 set- difference 都可以加上 :test 這一個 keyword 參數,用以改變判斷條件,而如果沒有使用 :test 改寫判斷條件的話,預設就是使用 eql 當作是判斷條件。
[編輯] 從Emacs開始
你可以使用Emacs編輯LISP代碼:Emaces在打開.lisp文件時總會自動進入LISP模式,不過如果我們的Emacs沒有成功進入這個狀態,可以通過輸入M-x lisp-mode做到。
我們也可以在Emacs下運行LISP:先確保在我們的私人路徑下可以運行一個叫 "LISP"的命令。例如,我們可以輸入:
ln -s /usr/local/bin/clisp ~/bin/lisp
然後在Emacs中輸入 M-x run-lip。我們可以向LISP發送先前的LISP代碼,做其它很酷的事情;在LISP模式下的任何緩衝輸入 C-h m可以得到進一步的信息。
實際上,我們甚至不需要建立鏈接。Emacs有一個變數叫inferior-lisp-program;所以我們可以把下面這行
(setq inferior-lisp-program "/usr/local/bin/clisp")
輸入到自己的 .emacs 文件中,Emacs就會知道在你輸入 M-x run-lisp時去哪裡尋找CLISP。
Allegro Common LISP 對使用 Emacs 有一個在線手冊。要使用它,將下面內容添加到你的 .emacs 文件中:
(setq load-path
(cons "/afs/cs/misc/allegro/common/omega/emacs" load-path))
(autoload 'fi:clman "fi/clman" "Allegro Common LISP online manual." t)
然後命令 M-x fi:clman 將提示你 LISP 主題並輸出相應的文檔。
注^_^:本文應該不是我翻譯的,因為整理的過程中大量參考了已有的翻譯版本:比如繁體中文版、簡體劉鑫翻譯版等 -- Dbzhang800
