> 1 1 >系统会打印它的值,跟着另一个提示符,表示它在等待更多的输入. 在这种情况下,打印出来的值和我们输入的一样. 象1这样的数叫做自身求值的. 当我们输入一个需要做些求值工作的表达式时,事情变得有趣起来. 例如,如果想 把两个数加起来,我们输入:
> (+ 2 3) 5在表达式(+ 2 3)中,+叫做操作符,数2和3叫做变元. 在日常生活中我们会把此表达式写为2 + 3,但在Lisp中我们把+写 在最前面,后面跟着变元,整个表达式被一对括号围住:(+ 2 3). 因为操作 符在前,这叫做前缀表示法. 一开始这样写表达式有点怪,但事实上这种表示法是 Lisp最好的东西之一. 比如,我们想把三个数加起来,用通常的表示法我们要写+两次:
2 + 3 + 4而在Lisp中我们仅需增加一个变元:
> (+ 2 3 4)通常我们用+,它必须有两个变元:一个在左边,一个在右边. 前缀表示法的 弹性意味着,在Lisp中,+可以接受任意数目的变元,包括零个:
> (+) 0 > (+ 2) 2 > (+ 2 3) 5 > (+ 2 3 4) 9 > (+ 2 3 4 5) 14因为操作符可以接受不同数目的变元,我们需要用括号指示表达式的开始和结束. 表达式可以嵌套. 即表达式中的变元本身可能是个复杂的表达式:
> (/ (- 7 1) (- 4 2)) 3用自然语言来说,七减一的结果被四减二的结果除. 另一个Lisp表示法的漂亮之处是:它无所不包. 所有的Lisp表达式要么是象1这样 原子(atom),要么是放在括号中由零个或多个表达式组成的表(list). 这些是合 法的Lisp表达式:
2 (+ 2 3) (+ 2 3 4) (/ (- 7 1) (- 4 2))正如我们将要看到的,所有的Lisp代码都采取这种形式. 象C这样的语言有着更复 杂的语法:算术表达式用中缀表示法;函数调用类似前缀表示法,自变量用逗号隔 开;表达式用分号隔开;而代码块用花括号分隔. 在Lisp中我们用单一的记号表达 所有这些概念.
> (quote (+ 3 5)) (+ 3 5)为了方便,Common Lisp定义'作为quote的简记法. 通过在任何表达式前面加上' 你能获得与调用quote同样的效果:
> '(+ 3 5) (+ 3 5)用简记法比用quote普遍得多. Lisp提供quote作为一种保护表达式以防被求值的手段. 下一节会解释这种保护 是多么有用. 从麻烦中解脱出来 如果你输入了一些Lisp不能理解的东西,它会打印一条出错信息并把你带到一个 叫中断循环(break loop)的顶层中去. 中断循环给了有经验的程序员弄清出错原 因的机会, 不过一开始你唯一需要知道的事是如何从中断循环中出来. 如何返回 顶层取决于你用的Lisp环境. 在这个假设的环境里,用:abort出来:
> (/ 1 0) Error: Division by zero. Options: :abort, :backtrace >> :abort >附录A展示了如何调试Lisp程序,以及一些最常见错误的例子.
> 'Artichoke ARTICHOKE符号(通常)不求值为自身,因此如果你想引用一个符号,请象上面那样用'引用它. 表表示为被括号包围的零个或多个元素. 元素可以是任何类型,包括表. 你必须 引用表,否则Lisp会以为它是函数调用:
> '(my 3 "Sons") (MY 3 "Sons") > '(the list (a b c) has 3 elements) (THE LIST (A B C) HAS 3 ELEMENTS)请注意一个引号保护整个表达式,包括里面的表达式. 你可以调用list来构造表. 因为list是一个函数,它的变元被求值. 这是+调用在 list调用里的例子:
> (list 'my (+ 2 1) "Sons") (MY 3 "Sons")现在我们处于欣赏Lisp最非同寻常特征之一的位置上. Lisp程序表达为表. 如果 变元的机动性和优雅性没能说服你Lisp记号是一种有价值的工具,这点应该能使 你信服. 这意味着Lisp程序可以生成Lisp代码. Lisp程序员能(而且经常)为自己 编写能写程序的程序. 我们到第10章才考虑这种程序,但即使在现阶段理解表达式和表的关系也是很重 要的,而不是被它们弄糊涂. 这就是为何我们使用quote. 如果一个表被引用了, 求值返回它本身; 如果没有被引用,它被认为是代码,求值返回它的值:
> (list '(+ 2 1) (+ 2 1)) ((+ 2 1) 3)此处第一个变元被引用了,所以生成了一个表. 第二个变元没有被引用,视之为函 数调用,得出一个数字. 在Common Lisp中有两种方法表示空表. 你可用一对不包含任何东西的括号来表 示空表,或用符号nil来表示它. 你用哪种方法表示空表都没有关系,不过它会被 显示成nil:
> () NIL > nil NIL你不必引用nil(虽然这也没什么害处)因为nil求值到自身.
> (cons 'a '(b c d)) (A B C D)我们可以通过把新元素cons到空表来构造新表. 我们在上一节见到的list函数只 不过是一个把几样东西cons到nil上去的方便办法:
> (cons 'a (cons 'b nil)) (A B) > (list 'a 'b) (A B)基本的提取表中元素的函数是car和cdr.1 表的car就是它的第一个元素,而 cdr是第一个元素后面的所有东西:
> (car '(a b c)) A > (cdr '(a b c)) (B C)你能用car和cdr的组合来取得表中任何元素. 如果你想取第三个元素,可以这样:
> (car (cdr (cdr '(a b c d)))) C但是,你可以用third更容易地做同样的事:
> (third '(a b c d)) C
> (listp '(a b c)) T一个函数叫做断言如果它的返回值被解释成真或假. Common Lisp的断言的名 字通常以p结尾. 假在Common Lisp中用nil(空表)来表示. 如果我们传给listp的变元不是表,它返 回nil:
> (listp 27) NIL因为nil扮演两个角色,函数null返回真如果它的变元是空表:
> (null nil) T而函数not返回真如果它的变元是假:
> (not nil) T它们完全做的是同样的事情. 要if是Common Lisp中最简单的条件语句. 它一般接受三个变元:一个测试表达式, 一个then表达式和一个else表达式. 测试表达式被求值. 如果它返回真,则then 表达式被求值并返回结果. 如果它返回假,则else表达式被求值并返回它的结果:
> (if (listp '(a b c)) (+ 1 2) (+ 5 6)) 3 > (if (listp 27) (+ 1 2) (+ 5 6)) 11就象quote,if是特殊操作符. 它不能用函数来实现,因为函数调用的变元总是要 求值的,而if的特点是只有最后两个变元中的一个被求值. if的最后一个变元是可选的. 如果你省略它,它缺省为nil:
> (if (listp 27) (+ 2 3)) NIL虽然t是真的缺省表示,任何不是nil的东西在逻辑上下文中被认为是真:
> (if 27 1 2) 1逻辑操作符and和or就象条件语句. 两者都接受任意数目的变元,但只求值能够确 定返回值的数目的变元. 如果所有的变元都是真(不是nil),那么and返回最后变 元的值:
> (and t (+ 1 2)) 3但如果其中一个变元是假,所有它后面的变元都不求值了. or也类似,只要它碰到 一个是真的变元就继续求值了. 这两个操作符是宏. 就象特殊操作符,宏可以规避通常的求值规则. 第10章解释 如何编写你自己的宏.
> (defun our-third (x) (car (cdr (cdr x)))) OUR-THIRD第一个变元表示函数名将是our-third. 第二个变元,表(x),说明函数将接受一个 变元:x. 象这样用作占位符的符号叫做变量. 当变量代表传给函数的变元, 就象x所做的,它又叫做参数. 定义的其余部分,(car (cdr (cdr x))),即通常所说的函数体. 它告诉Lisp,为了 计算函数的返回值,它该做些什么. 所以,对我们给出的作为变元的任何x,调用 our-third会返回(car (cdr (cdr x))):
> (our-third '(a b c d)) C既然我们看到了变量,就更容易理解什么是符号了. 他们是变量名,是一种有自己 名称的对象. 这就是为什么符号要象表一样必须被引用. 表必须被引用是因为不 如此的话,它就会被当作代码;符号必须被引用是因为不如此的话,它就会被当作 变量. 你可以把函数定义想象成某个Lisp表达式的一般形式. 下面的表达式测试1和4之 和是否大于3:
> (> (+ 1 4) 3) T通过把这些特殊数字换成变量,我们可以写一个函数测试任何两个数之和是否大 于第三个:
> (defun sum-greater (x y z) (> (+ x y) z)) SUM-GREATER > (sum-greater 1 4 3) TLisp对程序,过程或函数不加区别. 函数做了所有的事情(事实上构成了语言本身 的大部分). 你可以认为你的函数中的一个是主函数,但通常你能在顶层里调用任 何一个函数. 这意味着,当你写程序的时候,你能一小段一小段地测试它们.
(defun our-member (obj lst) (if (null lst) nil (if (eql (car lst) obj) lst (our-member obj (cdr lst)))))断言eql测试它的两个变元是否相同;除此之外,定义中所有东西我们以前都见过. 这是它的运行情况:
> (our-member 'b '(a b c)) (B C) > (our-member 'z '(a b c)) NILour-member的定义符合下面的自然语言描述. 为了测试一个对象obj是否是表lst 的成员,我们
(defun our-member (obj lst) (if (null lst) nil (if (eql (car lst) obj) lst (our-member obj (cdr lst)))))如果代码适当地缩进,他就没有困难了. 你可以忽略大部分的括号而读懂它:
defun our-member (obj lst) if null lst nil if eql (car lst) obj lst our-member obj (cdr lst)事实上,当你在纸上写Lisp代码的时候,这就是一个可行的办法. 以后你输入的时 候,可以充分利用编辑器的匹配括号的功能.
> (format t "~A plus ~A equals ~A.~%" 2 3 (+ 2 3)) 2 plus 3 equals 5. NIL注意两样东西打印在这儿. 第一行是format打印的. 第二行是format调用的返回 值,就象通常一样由顶层打印. 通常象format这样的函数不会直接在顶层,而 是在程序内部被调用,因此返回值就不会被看见. format的第一个变元t表示输出将被送到缺省的地方去. 通常这会是顶层. 第二 个变元是充当输出模板的字符串. 在它里面,每个 A*表示一个将被填充的位置, 而 %表示新行符. 这些位置依次被后面的变元的值填充. 标准的输入函数是read. 当没有变元时,它从缺省的地方--通常是顶层--读入. 下面这个函数提示用户输入,然后返回任何输入的东西:
(defun askem (string) (format t "~A" string) (read))它运行如下:
> (askem "How old are you? ") How old are you? 29 29请记住read会永远等在那儿直到你输入什么东西并(通常要)敲入回车. 因此调用 read而不打印明确的提示信息是不明智的,否则你的程序会给人以已经死掉的印 象,但实际上它在等待输入. 第二个要了解read的是它非常强大:它是一个完整的Lisp语法分析器. 它并不是 读入字符再把它们当作字符串返回. 它分析所读到的东西,并返回所产生的Lisp 对象. 在上例中, 它返回一个数. askem虽然很短,但它展示了一些我们以前在函数定义中没有看到的内容. 它的函 数体包含多个表达式. 函数体可以包含任意多个表达式,当函数被调用时,它们依 次被求值,函数会返回最后一个表达式的值. 在以前的章节中,我们坚持所谓的``纯粹''的Lisp--即没有副作用的Lisp. 副作 用是指作为表达式求值的后果改变了外部世界的状态. 当我们对一个纯粹的Lisp 表达式,例如(+ 1 2)求值,没有出现副作用;它仅返回一个值. 但当我们调用 format,它不仅返回值,还打印了一些东西. 这是一种副作用. 如果我们要写没有副作用的代码,那么定义有多个表达式的函数体就没有什么意 义. 最后一个表达式的值作为函数的返回值被返回了,但前面的表达式的值都被 扔掉了. 如果这些表达式没有副作用,你就不知道为什么Lisp要费劲去计算它们.
> (let ((x 1) (y 2)) (+ x y)) 3一个let表达式有两部分. 第一部分是一列创造新变量的指令,每个形如(变量 表 达式). 每个变量会被赋予相应的表达式的值. 在上例中,我们创造了两个变量x 和y,它们分别被赋予初值1和2. 这些变量只在let的体内有效. 变量和值的列表的后面是一组表达式,它们将被依次求值. 在此例中,只有一个表 达式:对+的调用. 最后一个表达式的值作为let的值被返回. 下面是一个使用let 的更具选择性的askem的版本:
(defun ask-number () (format t "Please enter a number. ") (let ((val (read))) (if (numberp val) val (ask-number))))此函数造了变量val来存放read返回的对象. 因为它有此对象的名称,它可以 在作出是否要返回对象之前察看一下你的输入值. 你可能已经猜到,numberp是测 试它的自变量是否是数字的断言. 如果用户输入的不是数字,ask-number调用它自己. 结果产生了一个坚持要得到 一个数的函数:
> (ask-number) Please enter a number. a Please enter a number. (ho hum) Please enter a number. 52 52象目前我们看到的变量都叫做局部变量. 它们只在特定的环境中是有效的. 另外 有一类叫做全局变量的变量,它们在任何地方都是可见的.3 通过传给defparameter一个符号和一个值,你可以构造全局变量:
> (defparameter *glob* 99) *GLOB*这样的变量可以在任何地方存取,除非在一个表达式中,定义了一个相同名字的局 部变量. 为了避免这种情况的出现,习惯上全局变量的名字以星号开始和结束. 我们刚才定义的变量可读作``星-glob-星''. 你还可以用defconstant定义全局常数:
(defconstant limit (+ *glob* 1))你不需要给常数起一个与众不同的名字,因为如果使用相同的名字作为变量,就会 出错. 如果你想知道某个符号是否是全局变量或常数的名字,请用boundp:
> (boundp '*glob*) T
> (setf *glob* 98) 98 > (let ((n 10)) (setf n 2) n) 2如果第一个自变量不是局部变量的名字,它被认为是全局变量:
> (setf x (list 'a 'b 'c)) (A B C)即你可以通过赋值隐含地新建全局变量.不过在源文件中明确地使用 defparameter
> (setf (car x) 'n) N > x (N B C)setf的第一个自变量几乎可以是任何涉及特定位置的表达式. 所有这样的操作符在 附录D中都被标记为``settable''. 你可以给setf偶数个自变量. 形如
(setf a b c d e f)的表达式相当于连续三个单独的setf调用:
(setf a b) (setf c d) (setf e f)
> (setf lst '(c a r a t)) (C A R A T) > (remove 'a lst) (C R T)为什么不说remove从表中删除一个对象? 因为这不是它所做的事情. 原来的表没 有被改变:
> lst (C A R A T)那么如果你真想从表中删掉一些元素怎么办? 在Lisp中,你通常这样做类似的事 情:把表传给某个函数,然后用setf来处理返回值. 为了把所有的a从表x中删掉, 我们这样做:
(setf x (remove 'a x))函数化编程法本质上意味着避免使用诸如setf的函数. 乍一看连想象这种可能性都 很因难,别说试着去做了. 怎么能仅凭返回值就能构造程序? 完全不利用副作用是有困难的. 但随着学习的深入,你会惊讶地发现真正需要副 作用的地方极少. 你使用副作用越少,你也就越进步. 函数化编程最重要的优点之一是它允许交互式测试. 在纯粹的函数化代码中,当 你写函数的时候就可以测试它们. 如果它返回期望的值,你可以肯定它是正确的. 这些额外的信心,聚集在一起会产生巨大的影响. 当你在程序中修改了任何地方, 你会得到即时的转变. 而这种即时的转变会带来一种全新的编程风格. 就象电话 与信件相比,赋予我们新的通讯方式.
(defun show-squares (start end) (do ((i start (+ i 1))) ((> i end) 'done) (format t "~A ~A~%" i (* i i))))打印从start到end之间的整数的平方:
> (show-squares 2 5) 2 4 3 9 4 16 5 25 DONEdo宏是Common Lisp中最基本的迭代操作符. 就象let,do也会产生变量,它的第一 个自变量是关于变量规格的表. 表中的每个元素具有如下形式:
(variable initial update)其中variable是符号,而initial和update是表达式. 一开始每个变量会被赋予相 应的initial的值;在迭代的时候它会被赋予相应的update的值. show-squares中 的do仅产生了一个变量i. 第一次迭代的时候,i被赋予start的值,在以后的迭代 中它的值会每次增加1. do的第二个自变量是包含一个或多个表达式的表. 第一个表达式用来测试迭代是 否应该停止. 在上例中,测试是(> i end). 其余的表达式会在迭代停止后依次计 算,并且最后一个的值作为do的返回值. 因此show-squares总是会返回done. 余下的自变量组成了循环体. 它们在每次迭代的时候依次被求值. 在每次迭代的 时候变量先被更新,然后终止测试被计算,再是(如果测试失败)循环体被计算. 作为对比,这是递归版本的show-squares:
(defun show-squares (i end) (if (> i end) 'done (progn (format t "~A ~A~%" i (* i i)) (show-squares (+ i 1) end))))此函数中的唯一新面孔是progn. 它接受任意数量的表达式,对它们依次求值,然 后返回最后一个的值. 对一些特殊情况Common Lisp有更简单的迭代操作符. 比如,为了遍历表的所有元 素,你更可能用dolist. 这个函数返回表的长度:
(defun our-length (lst) (let ((len 0)) (dolist (obj lst) (setf len (+ len 1))) len))此处dolist接受形如(variable expression)的自变量,然后是表达式块. variable相继与expression返回的表中元素绑定,表达式块被计算. 因此上面的 循环在意思是,对lst中的每个obj,len增加1. 此函数的一个显然的递归版本是:
(defun our-length (lst) (if (null lst) 0 (+ (our-length (cdr lst)) 1)))即,如果表为空,它的长度就是0;否则它的长度是它的cdr的长度加上1. 此版本清 楚一些,但因为它不是尾递归的(见13.2节),它的效率不那么高.
> (function +) #<Compiled-Function + 17BA4E>这个模样很奇怪的返回值是函数在典型Common Lisp实现中可能的显示方式. 到目前为止我们涉及到的对象具有这样的特点:Lisp显示它们与我们输入的模样 是一致的. 此惯例不适合函数. 一个象+这样的内置函数在内部可能是一段机器 码. Common Lisp的实现可以选择任何它喜欢的外部表示方式. 就象我们用'作为quote的简记法,我们可以用#'作为function的简写:
> #'+ #<Compiled-Function + 17BA4E>此简记法叫做sharp-quote. 就象其它的对象,函数可以作为自变量传递. 一个接受函数作为自变量的是 apply. 它接受一个函数和一列自变量,并返回那个函数应用这些自变量后的结 果:
> (apply #'+ '(1 2 3)) 6 > (+ 1 2 3) 6它能接受任意数目的自变量,只要最后一个是表:
> (apply #'+ 1 2 '(3 4 5)) 15函数funcall能做同样的事情,不过它不需要把自变量放在表中:
> (funcall #'+ 1 2 3) 6宏defun创造一个函数并给它一个名字. 但函数并不是必须需要名字,因此我们也 不需要用defun来定义它们. 就象其它Lisp对象一样,我们可以直接引用函数. 为了直接引用一个整数,我们用一列数字;为了直接引用函数,我们用所谓的 lambda表达式. 一个lambda表达式是包含以下元素的表:符号lambda,一列参数, 然后是零个或多个表达式组成的函数体. 这个lambda表达式代表接受两个数并返回它们之和的函数:
(lambda (x y) (+ x y))(x y)是参数表,跟在它后面的是函数体. 可以认为lambda表达式是函数的名称. 就象普通的函数名,lambda表达式可以是 函数调用的第一个元素:
> ((lambda (x) (+ x 100)) 1) 101而通过在lamda表达式之前附加#',我们得到了相应的函数:
> (funcall #'(lambda (x) (+ x 100)) 1) 101此种表达法让我们使用匿名函数. lambda是什么? lambda表达式中的lambda不是操作符. 它仅是个符号.4 它在早期的Lisp方言里有一种作用:函 数的内部形态是表,因此区别函数和普通表的唯一办法是查看第一个元素是否是 符号lambda. 在Common Lisp中你能把函数表示为表,但它们在内部被表示成独特的函数对象. 因此lambda不再是必需的. 如果要求把函数
(lambda (x) (+ x 100))表示成
((x) (+ x 100))也没有什么矛盾,但Lisp程序员已经习惯了函数以符号lambda开始,因此Common Lisp保留了此传统.
> (typep 27 'integer) T当我们碰到各种内置类型时,我们会介绍它们.
a. (defun enigma (x) (and (not (null x)) (or (null (car x)) (enigma (cdr x))))) b. (defun mystery (x y) (if (null y) nil (if (eql (car y) x) 0 (let ((z (mystery x (cdr y)))) (and z (+ z 1))))))
a. > (car (x (cdr '(a (b c) d)))) B b. > (x 13 (/ 1 0)) 13 c. > (x #'list 1 nil) (1)
a. 接受一个正整数,并打印这么多数目的点. b. 接受一个表,返回符号a在表中出现的次数.
a. (defun summit (lst) (remove nil lst) (apply #'+ lst)) b. (defun summit (lst) (let ((x (car lst))) (if (null x) (summit (cdr lst)) (+ x (summit (cdr lst))))))
This document was generated using the LaTeX2HTML translator Version 2K.1beta (1.48)
Copyright © 1993, 1994, 1995, 1996,
Nikos Drakos,
Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999,
Ross Moore,
Mathematics Department, Macquarie University, Sydney.
The command line arguments were:
latex2html -split=0 acl2.tex
The translation was initiated by Dai Yuwen on 2003-07-29