CommonLisp学习笔记一
范式(form)
任何 Lisp系统都包含一个叫做顶层(toplevel)的交互式前端,你在顶层中输入Lisp表达式,系统打印它们的值。
1 | > 1 |
系统会打印它的值,跟着另一个提示符,表示它在等待更多的输入. 在这种情况下,打印出来的值和我们输入的一样. 象1这样的数叫做自身求值的
当我们输入一个需要做些求值工作的表达式时
1 | > (+ 2 3) |
在表达式(+ 2 3)中,+叫做操作符,数2和3叫做变元
Lisp中我们把+写 在最前面,后面跟着变元,整个表达式被一对括号围住:(+ 2 3). 因为操作 符在前,这叫做前缀表示法
一开始这样写表达式有点怪,我们想把三个数加起来,用通常的表示法我们要写+两次2 + 3 + 4
,而在Lisp中我们仅需增加一个变元:(+ 2 3 4)
1 | > (/ (- 7 1) (- 4 2)) |
求值
在Lisp中,+是一个函数,形如(+ 2 3)的表达式是函数调用. 当Lisp对函数调用求 值时,它做这样两步:
- 首先变元从左至右被求值. 在此例中,每个变元求值到自身,所以变元的值 分别是2和3.
- 变元的值传给以操作符命名的函数. 在此例中,即+函数,它返回5.
如果任何变元本身是函数调用,它们按上述规则求值. 这是对(/ (- 7 1) (- 4 2))求值时发生的情况:
- Lisp计算(- 7 1): 7求值为7,1求值为1. 它们被传给函数-,它返回6.
- Lisp计算(- 4 2): 4求值为4,2求值为2. 它们被传给函数-,它返回2.
- 6和2的值传给函数/,它返回3.
并不是所有的Lisp操作符都是函数,但大多数都是. 而函数总是按这种方式求值 的. 变元从左至右被求值,它们的值被传给函数,函数返回整个表达式的值. 这叫做Common Lisp的求值规则.一个不遵守上述规则的操作符是quote
1 | > (quote (+ 3 5)) |
为了方便,Common Lisp定义’作为quote的简记法. 通过在任何表达式前面加上’ 你能获得与调用quote同样的效果:
1 | > '(+ 3 5) |
用简记法比用quote普遍得多. Lisp提供quote作为一种保护表达式以防被求值的手段
1 | ;; 从麻烦中解脱出来 如果你输入了一些Lisp不能理解的东西,它会打印一条出错信息并把你带到一个 叫中断循环(break loop)的顶层中去. 中断循环给了有经验的程序员弄清出错原 因的机会,不过一开始你唯一需要知道的事是如何从中断循环中出来. 如何返回 顶层取决于你用的Lisp环境. 在这个假设的环境里,用:abort出来: |
数据
Lisp提供在其它语言中找得到的数据类型,一是整数,另一种和其它语言一样有的是字符串,整数 和字符串都求值到自身. 另两种我们不常在其它语言中发现的是符号和表. 符号是单词. 通常它们被转换成大写,不管你如何输入
1 | > 'Artichoke |
符号(通常)不求值为自身,因此如果你想引用一个符号,请象上面那样用'
引用它. 表表示为被括号包围的零个或多个元素. 元素可以是任何类型,包括表. 你必须引用表,否则Lisp会以为它是函数调用:
1 | > '(my 3 "Sons") |
请注意一个引号保护整个表达式,包括里面的表达式. 你可以调用list来构造表. 因为list是一个函数,它的变元被求值. 这是+调用在 list调用里的例子:
1 | > (list 'my (+ 2 1) "Sons") |
现在我们处于欣赏Lisp最非同寻常特征之一的位置上. Lisp程序表达为表.
Lisp程序表达为表. 如果变元的机动性和优雅性没能说服你Lisp记号是一种有价值的工具,这点应该能使你信服. 这意味着Lisp程序可以生成Lisp代码. Lisp程序员能(而且经常)为自己编写能写程序的程序.
在现阶段理解表达式和表的关系也是很重 要的,而不是被它们弄糊涂. 这就是为何我们使用quote. 如果一个表被引用了, 求值返回它本身; 如果没有被引用,它被认为是代码,求值返回它的值:
1 | > (list '(+ 2 1) (+ 2 1)) |
此处第一个变元被引用了,所以生成了一个表. 第二个变元没有被引用,视之为函 数调用,得出一个数字. 在Common Lisp中有两种方法表示空表. 你可用一对不包含任何东西的括号来表 示空表,或用符号nil来表示它. 你用哪种方法表示空表都没有关系,不过它会被 显示成nil:
1 | > () |
表的操作
函数cons构造表. 如果它的第二个变元是表,它返回一个新表,新表的第一个元素 就是第一个变元:
1 | > (cons 'a '(b c d)) |
我们可以通过把新元素cons到空表来构造新表. 我们在上一节见到的list函数只不过是一个把几样东西cons到nil上去的方便办法:
1 | > (cons 'a (cons 'b nil)) // > (cons 'a 'b) |
基本的提取表中元素的函数是car和cdr表的car就是它的第一个元素,而 cdr是第一个元素后面的所有东西:
1 | > (car '(a b c)) |
真值
符号t是Common Lisp中表示真的缺省值. 就象nil,t求值到自身. 函数listp返回 真如果它的变元是一个表:
1 | > (listp '(a b c)) |
一个函数叫做断言如果它的返回值被解释成真或假. Common Lisp的断言的名字通常以p结尾. 假在Common Lisp中用nil(空表)来表示. 如果我们传给listp的变元不是表,它返回nil:
1 | > (listp 27) |
因为nil扮演两个角色,函数null返回真如果它的变元是空表:
1 | > (null nil) |
而函数not返回真如果它的变元是假:
1 | > (not nil) |
它们完全做的是同样的事情. 要if是Common Lisp中最简单的条件语句. 它一般接受三个变元:一个测试表达式, 一个then表达式和一个else表达式
1 | > (if (listp '(a b c)) |
就象quote,if是特殊操作符. 它不能用函数来实现,因为函数调用的变元总是要 求值的,而if的特点是只有最后两个变元中的一个被求值. if的最后一个变元是可选的. 如果你省略它,它缺省为nil:
1 | > (if (listp 27) |
虽然t是真的缺省表示,任何不是nil的东西在逻辑上下文中被认为是真:
1 | > (if 27 1 2) |
逻辑操作符and和or就象条件语句. 两者都接受任意数目的变元,但只求值能够确定返回值的数目的变元. 如果所有的变元都是真(不是nil),那么and返回最后变元的值:
1 | > (and t (+ 1 2)) |
但如果其中一个变元是假,所有它后面的变元都不求值了. or也类似,只要它碰到一个是真的变元就继续求值了. 这两个操作符是宏. 就象特殊操作符,宏可以规避通常的求值规则
函数
你可以用defun来定义新的函数. 它通常接受三个以上变元:一个名字,一列参数, 和组成函数体的一个或多个表达式. 这是我们定义third的一种可能:
1 | > (defun our-third (x) |
1 | > (defun sum-greater (x y z) |
Lisp对程序,过程或函数不加区别. 函数做了所有的事情(事实上构成了语言本身 的大部分). 你可以认为你的函数中的一个是主函数,但通常你能在顶层里调用任 何一个函数. 这意味着,当你写程序的时候,你能一小段一小段地测试它们.
递归
我们在上一节中定义的函数还调用了其它函数为自己服务. 比如sum-greater调 用了+和>. 函数可以调用任何函数,包括它本身. 自己调用自己的函数是递归的.
1 | (defun our-member (obj lst) |
输入和输出
到目前为止,我们一直在利用顶层暗中使用i/o. 对实际的交互式的程序,这可能还不够.
我们看一些输入输出函数. Common Lisp中最一般的输出函数是format. 它接受两个以上变元:第一个表示输出到哪儿,第二个是字符串模板,剩下的变元通常是对象,它们的打印表示 (printed representation)将被插入到模板中去. 这是个典型的例子:
1 | > (format t "~A plus ~A equals ~A.~%" 2 3 (+ 2 3)) |
注意两样东西打印在这儿. 第一行是format打印的. 第二行是format调用的返回值
在它里面,每个 A*表示一个将被填充的位置, 而 %表示新行符. 这些位置依次被后面的变元的值填充.
标准的输入函数是read. 当没有变元时,它从缺省的地方–通常是顶层–读入. 下面这个函数提示用户输入,然后返回任何输入的东西:
1 | (defun askem (string) |
请记住read会永远等在那儿直到你输入什么东西并(通常要)敲入回车. 因此调用 read而不打印明确的提示信息是不明智的,否则你的程序会给人以已经死掉的印象,但实际上它在等待输入.
第二个要了解read的是它非常强大:它是一个完整的Lisp语法分析器. 它并不是读入字符再把它们当作字符串返回. 它分析所读到的东西,并返回所产生的Lisp 对象.
在上例中, 它返回一个数. askem虽然很短,但它展示了一些我们以前在函数定义中没有看到的内容. 它的函数体包含多个表达式. 函数体可以包含任意多个表达式,当函数被调用时,它们依次被求值,函数会返回最后一个表达式的值. 在以前的章节中,我们坚持所谓的``纯粹’’的Lisp–即没有副作用的Lisp. 副作用是指作为表达式求值的后果改变了外部世界的状态. 当我们对一个纯粹的Lisp 表达式,例如(+ 1 2)求值,没有出现副作用;它仅返回一个值. 但当我们调用 format,它不仅返回值,还打印了一些东西. 这是一种副作用.
变量
局部变量
1 | > (let ((x 1) (y 2)) |
一个let表达式有两部分. 第一部分是一列创造新变量的指令,每个形如(变量 表达式).
最后一个表达式的值作为let的值被返回.
下面是一个使用let 的更具选择性的askem的版本:
1 | (defun ask-number () |
1 | > (ask-number) |
构造全局变量(习惯上全局变量的名字以星号开始和结束)
通过传给defparameter一个符号和一个值
1 | > (defparameter *glob* 99) |
还可以用defconstant定义全局常数
1 | (defconstant limit (+ *glob* 1)) |
判断当前是否存在全局变量或常数的名字,请用boundp
1 | > (boundp '*glob*) |
赋值
Common Lisp中最普通的赋值操作符是setf. 我们可以用它对全局或局部变量进 行赋值:
1 | > (setf *glob* 98) |
如果第一个自变量不是局部变量的名字,它被认为是全局变量:
1 | > (setf x (list 'a 'b 'c)) |
即你可以通过赋值隐含地新建全局变量.不过在源文件中明确地使用 defparameter是较好的风格.
你能做的远不止给变量赋值. setf的第一个自变量不但可以是变量名,还可以是 表达式. 在这种情况下,第二个自变量的值被插入到第一个所涉及到的位置:
1 | > (setf (car x) 'n) |
setf的第一个自变量几乎可以是任何涉及特定位置的表达式. 所有这样的操作符在 附录D中都被标记为``settable’’. 你可以给setf偶数个自变量. 形如
1 | (setf a b |
的表达式相当于连续三个单独的setf调用:
1 | (setf a b) |
函数化编程法
函数化编程法的意思是编写通过返回值来工作的程序,而不是修改什么东西.
它是 Lisp中占支配地位的范例. 大多数Lisp内置函数被调用是为了得到它们的返回值, 而不是它们的副作用.
1 | > (setf lst '(c a r a t)) |
为什么不说remove从表中删除一个对象? 因为这不是它所做的事情. 原来的表没 有被改变:
1 | > lst |
那么如果你真想从表中删掉一些元素怎么办? 在Lisp中,你通常这样做类似的事 情:把表传给某个函数,然后用setf来处理返回值. 为了把所有的a从表x中删掉, 我们这样做:
1 | (setf x (remove 'a x)) |
函数化编程法本质上意味着避免使用诸如setf的函数.
完全不利用副作用是有困难的. 但随着学习的深入,你会惊讶地发现真正需要副 作用的地方极少. 你使用副作用越少,你也就越进步.
函数化编程最重要的优点之一是它允许交互式测试. 在纯粹的函数化代码中,当你写函数的时候就可以测试它们. 如果它返回期望的值,你可以肯定它是正确的.
迭代
当我们想做一些重复的事情时,用迭代比用递归更自然些.
1 | (defun show-squares (start end) |
打印从start到end之间的整数的平方:
1 | > (show-squares 2 5) |
do宏是Common Lisp中最基本的迭代操作符. 就象let,do也会产生变量,它的第一 个自变量是关于变量规格的表. 表中的每个元素具有如下形式: (variable initial update)
其中variable是符号,而initial和update是表达式. 一开始每个变量会被赋予相 应的initial的值;在迭代的时候它会被赋予相应的update的值.
作为对比,这是递归版本的show-squares:
1 | (defun show-squares (i end) |
此函数中的唯一新面孔是progn. 它接受任意数量的表达式,对它们依次求值,然后返回最后一个的值.
对一些特殊情况Common Lisp有更简单的迭代操作符. 比如,为了遍历表的所有元素,你更可能用dolist. 这个函数返回表的长度
1 | (dolist (n '(1 2 3 4 5 6 7 8 9)) |
函数作为对象
函数在Lisp中就象符号,字符串和表一样,是常规的对象. 如果我们给function一个函数的名字,它会返回相关的对象. 就象quote,function是特殊操作符,因此我们不必引用自变量:
1 | > (function +) |
这个模样很奇怪的返回值是函数在典型Common Lisp实现中可能的显示方式. 到目前为止我们涉及到的对象具有这样的特点:Lisp显示它们与我们输入的模样是一致的. 此惯例不适合函数.
我们用’作为quote的简记法,我们可以用#’作为function的简写:
1 | #'+ |
此简记法叫做sharp-quote. 就象其它的对象,函数可以作为自变量传递.
一个接受函数作为自变量的是 apply. 它接受一个函数和一列自变量,并返回那个函数应用这些自变量后的结果:
1 | > (apply #'+ '(1 2 3)) |
它能接受任意数目的自变量,只要最后一个是表:
1 | > (apply #'+ 1 2 '(3 4 5)) |
函数funcall能做同样的事情,不过它不需要把自变量放在表中:
1 | > (funcall #'+ 1 2 3) |
宏defun创造一个函数并给它一个名字. 但函数并不是必须需要名字,因此我们也不需要用defun来定义它们. 就象其它Lisp对象一样,我们可以直接引用函数.
为了直接引用一个整数,我们用一列数字;为了直接引用函数,我们用所谓的 lambda表达式.
一个lambda表达式是包含以下元素的表:符号lambda,一列参数, 然后是零个或多个表达式组成的函数体. 这个lambda表达式代表接受两个数并返回它们之和的函数:
1 | (lambda (x y) |
1 | > ((lambda (x) (+ x 100)) 1) |
而通过在lamda表达式之前附加#’,我们得到了相应的函数:
1 | > (funcall #'(lambda (x) (+ x 100)) |
lambda是什么? lambda表达式中的lambda不是操作符. 它仅是个符号. 它在早期的Lisp方言里有一种作用:函数的内部形态是表,因此区别函数和普通表的唯一办法是查看第一个元素是否是符号lambda. 在Common Lisp中你能把函数表示为表,但它们在内部被表示成独特的函数对象. 因此lambda不再是必需的. 如果要求把函数
(lambda (x) (+ x 100))
表示成((x) (+ x 100))
也没有什么矛盾,但Lisp程序员已经习惯了函数以符号lambda开始,因此Common Lisp保留了此传统.
类型
Lisp用非同寻常的灵活手段来处理类型. 在许多语言中,变量是有类型的,你得指 定变量的类型才能使用它. 在Common Lisp中,值具有类型,而不是变量.
你可以假想每个对象都贴了指明它的类型的标签. 这种方法叫做显示类型. 你不需要去声明变量的类型,因为变量可以装任何类型的对象.
数27
是类型fixnum,integer,rational,real,number,atom,和t,以一般 性的增长为序. (Numeric类型在第9章中讨论)类型t是所有类型的超集,因此任何对象都是类型t.
函数typep接受一个对象和一个类型说明符,如果对象是那种类型就返回真:
1 | > (typep 27 'integer) |
总结
- Lisp是交互式语言. 如果你在顶层输入表达式,Lisp会打印它的值.
- Lisp程序由表达式组成. 表达式可以是一个原子,或是一个表, 表的第一 个元素是操作符,后面跟着零个或多个自变量. 前缀表达式意味着操作符可接受任意多个自变量.
- Common Lisp函数调用的求值规则:从左至右对自变量求值,然后把这些值传给由操作符表示的函数. quote有它自己的求值规则:它原封不动地返回自变量.
- 除了通常的数据类型,Lisp还有符号和表. 由于Lisp程序由表组成,很容易编写能写程序的程序.
- 三个基本的表处理函数是cons:它创造一个表;car:它返回表的头一个元素; cdr:它返回第一个元素之后的所有东西.
- 在Common Lisp里, t表示真,nil表示伪. 在逻辑上下文中,除了nil之外的任何东西都算作真. 基本的条件语句是if. and和or操作符就象条件语句.
- Lisp主要是由函数构成的. 你可用defun来定义新的函数.
- 调用自己的函数是递归的. 递归函数应该被认为是一个过程而不是机器.
- 括号不是个问题,因为程序员利用缩进来读写Lisp.
- 基本的i/o函数是read:它包含了完整的Lisp语法分析器,和format:它基于模板产生输出.
- 你可以用let创造新的局部变量,用defparameter创造新的全局变量.
- 赋值操作符是setf. 它的第一个自变量可以是表达式.
- 函数化编程法–它意味着避免副作用–是Lisp中占支配地位的范例.
- 基本的循环操作符是do.
- 函数是常规的Lisp对象. 它们可以作为自变量被传递,可以表示成lambda 表达式.
- 值有类型,而变量没有类型
练习
- 解释以下表达式求值后的结果:
- a. (+ (- 5 1) (+ 3 7))
- b. (list 1 (+ 2 3))
- 给出3种不同的能返回(a b c)的cons表达式
- 用car和cdr定义一个函数,它返回表的第四个元素.
- 定义一个函数,它接受两个自变量,返回两个中较大的一个.
- 这些函数做了什么?
1
2
3
4(defun enigma (x)
(and (not (null x))
(or (null (car x))
(enigma (cdr x)))))1
2
3
4
5
6
7(defun mystery (x y)
(if (null y)
nil
(if (eql (car y) x)
0
(let ((z (mystery x (cdr y))))
(and z (+ z 1)))))) - 在下面的表达式中,x处应该是什么可得出结果?
1
2> (car (x (cdr '(a (b c) d))))
B1
2> (x 13 (/ 1 0))
131
2> (x #'list 1 nil)
(1) - 只用本章介绍的操作符,定义一个函数,它接受一个表作为自变量,并返回t 如果表的元素中至少有一个类型是表.
- 给出函数的迭代和递归版本:它
- a. 接受一个正整数,并打印这么多数目的点.
- b. 接受一个表,返回符号a在表中出现的次数.
- 一位朋友想写一个函数,它返回表中所有非nil元素之和. 他写了此函数的 两个版本, 但没有一个能正确工作. 请指出错误在哪里,并给出正确的版本:
1
2
3(defun summit (lst)
(remove nil lst)
(apply #'+ lst))1
2
3
4
5(defun summit (lst)
(let ((x (car lst)))
(if (null x)
(summit (cdr lst))
(+ x (summit (cdr lst))))))
答案
1.
- 14
- (1 5)
1.
1
2
3(cons 'a (cons 'b (cons 'c nil)))
(cons 'a '(b c))
(cons 'a (cons 'b '(c)))
1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14(defun getForth (lst)
(car (cdr (cdr (cdr lst)))))
(defun getForth (lst n)
(let ((result lst))
(do ((i 2 (+ i 1)))
((> i n) 'done)
(setf result (cdr result))
)
(car result)
)
)
(getFour '(1 2 3 4 5 6 7 8 9 11) 5)
1.
1
2
3
4
5
6
7(defun getMax (a b)
(if (> a b)
(format t "~A" a)
(format t "~A" b)
)
)
(getMax 1 3)
1.
- 判断 x 列表中是否有 nil 元素
- 查找 x 在列表 y 中的下标,如果没有则为 nil
1.
- car
- or
- or ‘(1) 或 apply
1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14(defun checkList (x)
(and x
(or (listp (car x))
(checkList (cdr x)))))
(defun checkList (x)
(if (null x)
nil
(if (listp (car x))
t
(checkList (cdr x)))))
; (checkList '(1 2)) // nil
; (checkList '(1 2 '(1))) // T
1.
1
2
3
4
5
6
7
8; 接受一个正整数,并打印这么多数目的点---递归
(defun print-point (x)
(if (= 0 x)
nil
(progn (format t "~%.")
(print-point (- x 1)))))
> (print-point 5)
1
2
3
4
5
6
7; 接受一个正整数,并打印这么多数目的点---迭代
(defun print-point (x)
(do ((i 0 (+ i 1)))
((= i x) 'done)
(format t "~%.")))
> (print-point 5)
1
2
3
4
5
6
7
8; 接受一个列表,并返回 a 在列表里所出现的次数--递归
(defun count-a (lst)
(if (null lst)
0
(+ (if (eql 'a (car lst)) 1 0)
(count-a (cdr lst)))))
> (count-a '(a a a 1 3 4 a))
1
2
3
4
5
6
7
8
9
10; 接受一个列表,并返回 a 在列表里所出现的次数--迭代
(defun count-a (lst)
(let ((count 0))
(dolist (n lst)
(setf count
(+ (if (eql 'a n) 1 0)
count)))
count))
(count-a '(a a a a 1 23))
1.
1
2
3
4; 因为 remove 并不会改变 lst 本身
(defun summit (lst)
(setf nlst (remove nil lst))
(apply #'+ nlst))
1
2
3
4
5
6
7
8; 因为递归没有边界退出分支
(defun summit (lst)
(if (null lst)
0
(let ((x (car lst)))
(if (null x)
(summit (cdr lst))
(+ x (summit (cdr lst)))))))