Common LISP Hints
关于Common LISP Hints
作者:Geoffrey J. Gordon <[email protected]>
修订:Bruno Haible <[email protected]> 与 Peter Van Eynde <[email protected]>
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 设为数字 6 。 ((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 scoped ,也就是说,如果 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 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 9
最后的 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) ;funcall 会调用以第一个参数为名的函数,并把 funcall 的其它参数当作是要调用的函数的传入参数。 7 > (apply #'+ 3 4 '(3 4)) 14 ;apply 就像 funcall 一样,除 apply 最后一个参数须要是链表;这最后链表里面的元素,就像是在使用 funcall 时的额外参数一样。
> (mapcar #'not '(t nil t nil t nil)) (NIL T NIL T NIL T)
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-lisp 。我们可以向 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