Common LISP Hints
关于Common LISP Hints
Geoffrey J. Gordon
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 手册。
Forms(窗体) 跟 the Top-Level Loop(最高层评估值循环)
你一行行打字,所输入给 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)) ...))
Dynamic Scoping
let 跟 let* 这样的特殊窗体提供了 lexical scoping ,那就像你在写 C 或是 Pascal 程序 所预期一样的变量可视范围。而还有一种 Dynamic scoping 就如同 BASIC 语言所提供的一样,如果你指定一个变量值给 dynamically scoped 的变量,那不管你在何时去读取变量的值,都会是一开始指定的那个变量值,除非你有给它另一个新变量值,以取代之。译注:既,全域变量 ,参考 http://www.supelec.fr/docs/cltl/clm/node67.html 。
在 Lisp 里面,这些 dynamically scoped 的变数被称做是特殊变数(special variables),你可以透过 defvar 特殊窗体来定义特殊变量。下面是一些 lexically 跟 dynamicallyscoped 变数的例子。
在下面的这么范例里面, 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_ 这本书。
Arrays(数组)
函数 make-array 可以产生数组,而函数 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)
Structures(结构)
Lisp 的结构就类似 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
Booleans(布尔值) and Conditionals(控制判断条件)
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 。″
Iteration(重复结构)
在 Lisp 中最简单的重复结构就是 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* 而已。
Funcall, Apply, and 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 (未具名函数)。
> #'(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)
Sorting(排序)
Lisp 提供了两个主要的排序函数: 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))) (译注:也就是 a 与 b 套用在排序函数时相等),sort 或许有可能在排序之后,会对调 a 与 b 的顺序。而 stable-sort 跟 sort 使用方式完全一样,除了 stable-sort 保证对于相同的元素必定不会对调顺序。
请务必注意: sort 允许破坏他的输入参数序列,所以如果原始传入参数对你而言是很重要的,请先利用 copy-list 或 copy-seq 做好备份。
Equality(相等)
Lisp 对于"相等"的意义有很多种类型。 数值上的相等是用 = 来判别。两个字符则是用 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 当作是判断条件。
Getting Started with Emacs
You can use Emacs to edit LISP code: most Emacses are set up to enter LISP mode automatically when they find a file which ends in .lisp, but if yours isn't, you can type M-x lisp-mode. You can run LISP under Emacs, too: make sure that there is a command in your path called "lisp" which runs your favorite LISP. For example, you could type
ln -s /usr/local/bin/clisp ~/bin/lisp
Then in Emacs type M-x run-lisp. You can send LISP code to the LISP you just started, and do all sorts of other cool things; for more information, type C-h m from any buffer which is in LISP mode. Actually, you don't even need to make a link. Emacs has a variable called inferior-lisp-program; so if you add the line
(setq inferior-lisp-program "/usr/local/bin/clisp")
to your .emacs file, Emacs will know where to find CLISP when you type M-x run-lisp.