AI编程范式 第1章 介绍一下Lisp

第一部分Common Lisp介绍
第1章 介绍一下Lisp
你在学的时候觉得已经明白了,写的时候更加确信了解了,教别人的时候就更有自信的了。一直到你开始编程的时候才明白什么是真正的理解——Alan Perlis,耶鲁大学计算机科学家
本章是为了完全没有Lisp经验的人准备的。如果你在Lisp编程方面有足够的自信,可以快速浏览这一章或者直接跳过。本章会进展的比较快,没有编程经验的读者或者发现本章很难理解的读者请去寻找一些入门类的书籍来看看,在前言里有推荐。
计算机就是允许实行计算的机器。一个字符处理程序可以处理字符,就像计算器处理数字一样,原理都是相同的。两个环境都是,你提供输入(字符或者数字),并且定义一个操作(比如删除一个字符或者加上两个数字),之后得出一个结果(一份完整的文档或者计算结果)。
我们称任何计算机内存中的实体为一个可计算对象,简称对象。因此,字符,段落和数字都可以是对象。由于那些操作本身(删除或者增加)也必须在计算机内存中保存,所以也是对象。
正常来说,一个计算机用户和一个程序员的区别在于,用户给计算机提供的是新的输入,或者数据(字符或者数字),而程序员则是定义新的操作,或者程序,还有新的数据类型。每一个新的对象,或是数据或是程序,都必须按照之前定义的对象来定义。坏消息是,这个定义正确合理的过程可能是非常枯燥乏味的。好消息是每一个新的对象都可以在未来的对象定义中使用。因此,再复杂的程序也可以使用简单精简的对象来进行构建。本书包含了一些典型的AI问题,每一个问题都会慢慢分解成可管理的小部分,每一个部分都会由Common Lisp来具体描述。理想情况下,读者会通过学习这些例子学到足够的知识,破解新的AI问题。
让我们来考虑一个简单的计算例子:两个数字的和,简单说2+2,。如果我们手边有一个计算器,就可以输入2+2=,然后就会显示答案了。在使用波兰标志的计算器上,我们可以输入 2 2 +来得到同样的结果。Lisp也有计算功能,用户可以使用交互式对话来输入表达式,之后就可以看到答案。交互模式和其他大部分只有批处理模式的语言不同,批处理只能输入整个程序编译运行,然后才能看到结果。
只要轻按一下电源键,一个便携式计算机就可以开始工作。Lisp程序也是需要被打开的,但是具体的细节根据机器的不同有所区别,所以我就不解释你的Lisp如何工作了。假设我们已经成功打开了Lisp,某种的提示符就会出现。在我的计算机上,提示符就是符号“>”,这样就显示Lisp已经准备好接受输入了。所以我们面对的屏幕是这样子的:
>
现在我们输入算式,然后看看结果。很明显,Lisp的表达式和算术表达式有一些不一样:一个算式由括号列表组成,开头是操作的名字,之后是任意数量的操作数,或者参数。这种表达式叫做前缀表达式。
> (+ 2 2)
4
>
可见,Lisp打印答案,4,之后又输出另一个提示符,>,来接受接下来的输入。本书中的Lisp表达式的输入都是打印字符,和用户输入的字符是一样的。一般来说,程序员输入的字符是小写的,计算机输出的字符都是大写的。当然,像字符+和4是没有区别的。
为了节省书的空间,我们有时候吧输入和输出放在同一行,中间用一个箭头分割(=>),读者可以理解为相等于,也可以是看做用于在键盘上按下的回车键,表示输入结束。
> (+ 2 2) => 4
使用括号前缀表达式的好处之一就是括号可以清楚的标记表达式的开始和结束。如果我们想,我们可以给+添加更多的参数,他会将参数全部相加。
> (+ 1 2 3 4 5 6 7 8 9 10) =>55
接下来我们尝试更加复杂的算式:
> (- (+ 9000 900 90 9) (+ 5000 500 50 5)) =>4444
这个例子表示表达式是可以嵌套的。函数-的参数就是括号列表,而函数+的每一个参数都是原子。Lisp表达式可能和标准的数学表达不太一样,但是这种方式也是有好处的;表达式是由函数名后面加上参数组成的,所以+符号就不用重复出现了。比标记更加重要的是求值的规则。在Lisp中,列表的求值是首先对所有的参数求值,之后用函数操作参数,进而计算结果。这个求之规则比一般的数学求值要简单的多,数学中的求值需要记忆很多的规则,比如在求和和求差之前必须先进行加和除操作。我们接下来会看到,真实的Lisp求值规则会有点复杂,但是不会很过分。
有时候熟悉其他编程语言的程序员,会有一些先入为主的概念,会对学习Lisp造成阻碍。对于他们,有三点需要明确,第一,其他许多语言由表达式和语句的区分,比如表达式2+2,有一个值。但是一个语句,像x=2+2,就没有值。语句是有效果的,但是并不返回值。在Lisp中,是没有这样的分别的:没一个表达式都会返回一个值,有些表达式是有效果的,但是仍然会返回一个值。
第二,Lisp的语法规则比其他语言的规则简单。特别是标点符号更少:只有括号,引号(单引号,双引号,反引号),空格,还有逗号作为互相之间的分隔符。因此,在其它语言中,语句y=a*x+3会被解析成七个独立的符号,在Lisp中却被看做一个符号。为了构成一个标记的列表,我们在记号之间插入空格(y = a * x + 3)。虽然这不是一个合法的符号列表语句,但是是一个Lisp数据的对象。第三,很多语言是用分号作为语句的结尾,Lisp不需要分号,表达式是用括号来分界的。Lisp选择使用分号作为注释开始的标记,分号之后的这行的所有内容都是注释。
> (+ 2 2) ; this is a comment
4

1.1 符号计算

到现在为止,我们所做的数学计算和一个计算器能做的没有区别。Lisp比计算器强大的地方有两个:第一,它允许我们操作除了数字之外的其他对象。第二,后面的计算中,如果需要我们可以定义新的对象。慢慢来,一点点看看这两个重要的特性。
除开数字之外,Lisp也可以表示字符(字母),字符的串(字符串),和任意的字符,这些个字符可以被外部解释为任意的数学表达。Lisp也可以非原子类型的对象,也就是将多个对象包括近一个列表中宏。这项功能是语言本身的基础功能,很好的集成完毕了;事实上,Lisp的名字由来就是列表处理的意思。
下面呢是一个列表计算的例子:
> (append '(Pat Kim) '(Robin Sandy)) => (PAT KIM ROBIN SANDY)
这个表达式将两个名字的列表追加在一起。至于求值规则是和数字的求值一样的规则:应用这个函数(这里是append)到后面的参数上。
之前没有见过的,就是这个单引号('),他会对接下来要求值的表达式进行锁定,不加修改的返回。如果不加上单引号,例如表达式(Pat Kim),就会把Pat当成一个函数名然后应用到表达式Kim上。这不是我们想象出来的过程,单引号的功能就是使得Lisp将列表当做数据来看而不是一个函数调用来看。
> '(Pat Kim) ¬=> (PAT KIM)
在其他编程语言中(包括英语中),引号都是成对出现的:一个来标记开始,一个来标记结束。在Lisp中,一个单引号被用来标记表达式的开始。既然我们知道,一对括号已经用来规定表达式的开始和结束,那么后边用来标记结束的引号就显得多余了。引号可以用在列表,或者符号,甚至是其他任何对象上。下面是一些例子:
> 'John=> JOHN
> '(John 0 Public) => (JOHN 0 PUBLIC)
>'2 => 2
>2 => 2
> '(+ 2 2 ) => (+ 2 2 )
> (+ 2 2 ) => 4
> John => Error: JOHN is not a bound variable
> (John 0 Public) => Error: JOHN is not a function
'2求值是2是因为表达式加上引号,2求值为2是因为2本身求值就是2。一个结果,两个原由。对比之下,john的求值,加上引号的就是输出john,但是不加上引号的就会输出错误,因为对一个符号求值意味着在系统中这个符号是有所指向的,但是之前并没有什么对象赋值给john。
符号计算是允许互相嵌套的,甚至也可以和数字计算进行混合。下面的表达式以之前不太一样的方式建立了一个姓名的列表,他使用了内建的函数list。之后我们会看到另一个内建函数length,他会求出列表的元素个数。
> (append '(Pat Kim) (list '(John 0 Public) 'Sandy))
(PAT KIM (JOHN 0 PUBLIC) SANDY)
> (length (append '(Pat Kim) (list '(John 0 Public) 'Sandy)))
4
关于符号有非常重要的四点需要讲解:
第一Lisp对于对象的操作是不会附加任何信息的。比如,从人的视角来看,Robin Sandy很明显就是一个人的名字,还有John Q Public是一个人名,中间名,姓的组合。Lisp是没有这些个预设的概念的,对于Lisp来说,Robin也好xyzzy也好都只是符号而已。
第二为了进行上面的计算,必须先要了解一个Common Lisp中定义的函数,比如append,length,还有+函数。学习一门外语包括了记忆那门语言的词汇(或者知道上哪儿查询),还要对规范语言的基本规则和定义语义的基础进行了解。Common Lisp提供了超过700个内建函数。具体的很多函数需要读者去参考别的书籍,但是大部分重要的函数会在第一部分有所介绍。
第三Common Lisp是不区分大小写的,也就是说,John还是john或者是JoHn都是一个意思,打印出来都是JOHN。在环境中,变量print-case是可以控制打印出来的大小写的,默认是大写的,也可以设置成小写的。
第四构成符号的字符种类是非常繁多的:数字,字母,还有其他符号,比如加号和叹号。符号构成的规则说起来有点小复杂,但是一般来说,构成符号的都是字母,有时候会在单词之间用分隔符,-,或许在符号的最后会有数字结尾。有些程序猿在变量的命名方面会更加不拘小节,可能会包含类似于问号,美元符号等等的字符。例如,一个美元转日元的程序可能会这样命名$-to-yen或者$->yen。在Pascal或者C中,命名也许是DollarsToYen,或者dollarstoyen,dol2yen。当然这些规则之外有很多的例外,我们等他们出现的时候再解释吧。

1.2 变量

符号计算的基本概览之后,我们接下去看看,或许就是一门语言最最重要的一个特性:按照其他对象定义新的对象的能力,给新对象赋予名字以待后用。符号再一次扮演了一个重要的角色,用来给变量命名。一个变量可以存储一个值,这个值可以是任何Lisp对象。给变量赋值的方法有一个就是用setf函数:
> (setf p '(John 0 Public)) => (JOHN 0 PUBLIC)
> p => (JOHN 0 PUBLIC)
> (setf x 10) => 10
> (+ X x) => 20
> (+ x (length p)) ¬=> 13
在把值(John Q Public)赋值给变量p之后,我们可以使用变量的名字p来指向这个值。相似的,再把这个值赋值给变量x,我们就可以用x和p来指向这个值。
符号,在Common Lisp中也会被用来命名函数。每一个符号都可以被用来当做变量名或者函数名,或者两者都是。例如,append和length是函数名,但是没有作为变量的对应值,符号pi没有对应函数但是却又对应变量,它的值是3.1415926535897936(近似值)。

1.3 特殊形式

细心的读者可能会发现,setf违反了求值规则。之前我们提到的函数像+,-,还有append,他们的规则是先对所有的参数求值,之后再把函数应用到参数上。但是setf没有遵循这条规则,因为setf根本就不是一个函数。相反的,这是Lisp的基本句法,Lisp有一些为数不多的句法表达式。姑且称之为特殊形式。在其他语言中,也有相同目的的语句存在,也确实有一些相同意义的语法标记,比如if和loop。第一,Lisp的句法形式总是在列表中的第一个,后面跟着其他的符号。Setf就是这些特殊形式中的一个,所以(setf x 10)是一个特殊表达式。第二,特殊形式的表达式会返回一个值。相比其他的语言,是有效果但是不返回值的。
在对表达式(setf x (+ 1 2))求值的过程中,我们将符号x作为变量名,并且赋值(+ 1 2),也就是3.如果说setf是一个普通函数,那么操作的顺序就是先对两个参数求值,之后再使用两个值来进行计算,这显然不是我们想要的效果。Setf被称作特殊形式是因为他做的事情就是特殊的:如果没有setf,写一个函数给一个变量赋值就是完全不可能的事情。Lisp的核心哲学就是先提供一些为数不多的特殊形式,他们的功能是函数无法替代做到的,之后再由用户来写函数实现想实现的各种功能。
术语特殊形式的确是有些让人费解,他即表示setf又表示以setf开头的表达式。有一本书Common Lispcraft中作者就为了辨析两个意思把setf称作一个特殊函数,而特殊形式就用来指代setf的表达式。这种说法暗示了setf仅仅是另一种函数,一种第一个参数不求值的函数,这样的观点在Lisp还只是被看做一门解释语言的时候大行其道。现代的观点认为setf不应该被看做是一种特殊的函数,而是应该作为一种特殊的句法标记,由编译器来特别处理。因此,特殊形式(setf x (+ 2 1))的意思应该就是等同于C语言中的x = 2 + 1。本书中当有产生歧义的危险的时候,我们会将setf称作一个特殊形式操作符,而表达式(setf x 3)称作特殊形式表达式。
另外要说的是,引号仅仅是一个特殊形式的缩写。表达式’x等同于(quote x),这个特殊形式表达式求值是x。本章使用的特殊形式操作符见下表:

特殊形式操作符 功能含义
defun 定义函数
defparameter 定义特殊变量
setf 给变量赋值
let 绑定本地变量
case 分支选择
if 条件选择
function(#’) 指向一个函数
quote(‘) 引入常量数据
1.4 列表

到现在为止我们见到的列表相关函数有两个:append和length。列表的重要性值得我们在看看更多的列表操作函数:
> P => (JOHN 0 PUBLIC)
> (first p) =>¬ JOHN
> (rest p) => (0 PUBLIC)
> (second p) => 0
> (third p) => PUBLIC
> (fourth p) => NIL
> (length p) => 3
函数的命名也都蛮巧妙的,first,second,third,fourth:依次返回第1234个元素。Rest的意思就没那么明显,意思是除第一个元素之外的后续元素,符号nil和一堆括号()的意思是完全相同的,表示一个空列表。Nil同时也用来表示Lisp中的假,也就是false的值。因此表达式(fourth p)的值就是nil,因为p本来就没有第四个元素。请注意,列表的构成元素不一定要是原子,子列表也是可以的。
> (setf x '((1st element) 2 (element 3) ((4) 5))
((1ST ELEMENT) 2 (ELEMENT 3) ((4) 5)
> (length x) => 5
> (first x) => (1ST ELEMENT)
> (second x) => 2
> (third x) => (ELEMENT 3)
> (fourth x) => ((4))
> (first (fourth x)) => (4)
> (first (first (fourth x))) => 4
> (fifth x) => 5
> (first x) => (1ST ELEMENT)
> (second (first x)) => ELEMENT
我们已经学会怎么访问列表内部的元素了,完完全全新建一个列表也是可以的,例子:
> P => (JOHN Q PUBLIC)
> (cons 'Mr p) => (MR JOHN Q PUBLIC)
> (cons (first p) (rest p)) => (JOHN Q PUBLIC)
> (setf town (list 'Any town 'USA)) => (ANYTOWN USA)
¬> (list p 'of town 'may 'have 'already 'won!) =>
((JOHN Q PUBLIC) OF (ANYTOWN USA) MAY HAVE ALREADY WON!)
> (append p '(of) town '(may have already won!)) =>
(JOHN Q PUBLIC OF ANYTOWN USA MAY HAVE ALREADY WON!)
> P => (JOHN Q PUBLIC)
函数cons就是construct构造的缩写。接受的参数是一个元素加一个列表。(等一下我们再看第二个参数不是列表的情况)。之后cons就会构造一个新的列表,第一个元素就是第一个参数,之后的元素就是第二个参数中的元素。函数list,接受任意数量的参数,之后会按顺序形成一个列表。因此,append的参数必须是列表,而list的参数可能是列表或者原子。有一点很重要,这些函数都是创建一个新的列表,并不破坏原有的列表。表达式(append p q)的意思是创建一个完全全新的列表,用的就是p和q的元素,但是p,q本身是不会改变的。
现在我们暂且放下抽象的列表函数,来看一个简答的问题:如果以列表的形式给出一个人的名字,怎么提取出它的家族名呢?对于(JOHN Q PUBLIC),或许可以使用函数third,但是对于灭幼中间名的名字怎么办?Common Lisp中有一个函数叫做last;或许可以有效果,我们可以这么做:
> (last p) => (PUBLIC)
> (first (last p)) => PUBLIC
Last直接返回的不是最后一个元素本身,而是仅仅含有最后一个元素的列表。这样的设计好像有些有悖常理,不符合逻辑,实际上是这样。在ANSI Common Lisp中,last的定义是返回一个列表的最后n个元素,而个数这个选项的默认值就是1。因此(last p)=(last p 1)=(PUBLIC),而(last p 2)=(0 PUBLIC),这样子定义或许是有些违背常理。所以我们才需要last和first结合来完成这个功能,获取真实的最后一个元素。为了表示他的功能,我们给他一个名正言顺,叫做last-name函数。Setf是用来定义变量名字的,所以对于函数的定义并不适用。定义函数的方法将会在下一节讲到。

1.5 定义一个新的函数

特殊形式defun就是define function的缩写。先来定义一下上一节中的last-name函数。
(defun last-name (name)
"Select the last name from a name represented as a list."
(first (last name)))
给出一个新的函数名叫做last-name。参数列表中只有一个参数,(name),意思就是函数只接受一个参数,指向的就是名字。接下来双引号中的内容叫做文档字符串,用来说明函数是做什么用的。文档字符串本身并不参与计算,但是在调试和理解大型系统的时候是一个利器。函数定义的函数体就是(first (last name)),也就是之前用来获取p的家族名的程序。不同的地方在于我们这一次是要获取所有名字的家族名,不仅仅是p。
一般来说,函数的定义遵循下面的格式(文档字符串是可选的,其他部分是必须的):
(defun函数的名字 (参数列表 ...)
"文档字符串 "
函数体 ... )
函数名必须是一个符号,参数一般也是符号,函数体是由一个或者多个表达式所组成,函数被调用的时候这些歌表达式就会被求值。最后一个表达式的值返回,作为整个函数调用的值。
一个新的函数last-name定义之后,就可以像其他Lisp函数一样来使用了:
> (last-name p) => PUBLIC
> (last-name '(Rear Admiral Grace Murray Hopper)) =>HOPPER
> (last-name '(Rex Morgan MD)) => MD
> (last-name '(Spot)) => SPOT
> (last-name '(Aristotle)) => ARISTOTLE
最后三个例子指出了编程过程当中固有的限制。当我们说定义了一个函数last-name的时候,我们并不是真的认为每一个人都是有一个姓的;仅仅是在一个列表形式的名字上的一个操作而已。凭直觉讲,MD应该是一个头衔,Spot大概是一条狗的名字,而Aristotle亚里士多德生活在姓这个概念发明出来之前的年代。但是我们还是可以不断改进自己的程序last-name来适应这些例外的情况。
我们也可以定一个函数叫做first-name,虽然这个定义很多余(功能和first函数一样),但是显式重新定义也是可以的。之后就可以使用first-name来处理姓名列表,而first用来处理任意的列表,计算机的操作是完全相同的,但是我们作为程序员(或者是阅读程序的人),会少了很多困惑。定义像first-name这样的函数的另一个好处是,如果我们决定改变名字的表现,我们只需要更改first-name的定义。这可比在一个大型程序中更改first的应用来的方便。
(defun first-name (name)
"Select the first name from a name represented as a list."
(first name))
> p => (JOHN 0 PUBLIC)
> (first-name p) => JOHN
> (first-name '(Wilma Flintstone)) => WILMA
> (setf names '((John 0 Public) (Malcolm X)
(Admiral Grace Murray Hopper) (Spot)
(Aristotle) (A A Milne) (Z Z Top)
(Sir Larry Olivier) (Miss Scarlet)) =>
((JOHN 0 PUBLIC) (MALCOLM X) (ADMIRAL GRACE MURRAY HOPPER)
(SPOT) (ARISTOTLE) (A A MILNE) (Z Z TOP) (SIR LARRY OLIVIER)
(MISS SCARLET))
> (first-name (first names) => JOHN
最后一个表达式中,我们用的那个函数是先提取列表中的第一个元素,也就是一耳光列表,在提取这第一个元素的元素。

1.6 使用函数

One good thing about defining a list of names, as we did above, is that it makes it
easier to test our functions. Consider the following expression, which can be used to
test the 1 ast-name function:
上面定义的一大串名字的列表,接下来可以用来测试我们的函数。看看下面的表达式,就是用来测试last-name函数:
> (mapcar #'last-name names)
(PUBLIC X HOPPER SPOT ARISTOTLE MILNE TOP OLIVIER SCARLET)
井号加上单引号#’,这个标记是用来将符号转换成函数名的意思。和引号的功能类似。内建函数mapcar接受了两个参数,一个函数和一个列表。这个表达式返回一个列表,列表的每一个元素都是第二个参数中的元素经过第一个参数-函数,处理过后的结果。换句话说,mapcar调用等同于:
(list (last-name (first names))
(last-name (second names))
(last-name (third names))
.. . )
mapcar的名字来自于maps,定位,地图的意思,这是前半部分map,意味着后续函数会操作每一个列表的元素。后半部分car指的是Lisp函数car,first函数的老名字。Cdr是rest函数的老名字。Car和cdr是一个缩写,(contents of the address register)和(contents of the decrement register),这些名字的第一次使用是在一台IBM 704机器上,是Lisp的第一个版本实现。我很确信你也认为first和rest是个更好的名字,而且他们会在我们关于列表的讨论中一直替代car和cdr存在。但是我们在跳出列表视角,看是将两个看做是一对值的时候,还是会使用car和cdr的。
还有更多mapcar的例子:
> (mapcar #' - '0 2 3 4) => (-1 -2 -3 -4)
> (mapcar #'+ '0 234) '00 20 30 40)) => 01 22 33 44)
最后一个例子显示了,mapcar是可以接受三个参数的,在这种情况下,第一个参数应该是一个二元程序,也就是支持两个参数的输入,之后就可以处理对应的元素。一般来说mapcar会希望n元函数作为第一个参数,之后是n个列表。之后的操作就是函数收集每一个列表的元素,依次来。之后一个个处理,知道其中一个列表到了尽头。Mapcar就会返回一耳光所有函数返回值的列表作为结果。
现在我们理解了mapcar,接下来测试一下first-name函数:
> (mapcar #'first-name names)
(JOHN MALCOLM ADMIRAL SPOT ARISTOTLE A Z SIR MISS)
这些歌结果可能让人比较失望,因为有很多不正常的结果在。假如我们想要去掉那些头衔或者称呼,miss或者Admiral,做一个真正的名字的输出版本。记下来可以这么修改:
(defparameter *titles*
'(Mr Mrs Miss Ms Sir Madam Dr Admiral Major General)
"A list of titles that can appear at the start of a name.")
最新引入的特殊形式操作符叫做defparameter,用来定义一个参数,也就是一个变量,在计算过程中不改变。但是我们要加新东西的时候会更新(比如加上法语Mme或者军衔Lt.)。defparameter同时给出变量名和值,定义的变量之后的程序就可以使用。在这个例子中我们练习的选项就是提供一个文档字符串来描述变量。在Lisp程序员之间有个惯例就是将特殊变量的名字用星号给包裹起来,这也仅仅是一个惯例而已;在Lisp中,星号仅仅是一个字符,没有特别的含义。
我们接下来定义一个新的first-name版本,代替之前的版本。这个版本的改进仅仅是我们可以更改变量的值,也可以改变函数的值。这个定义的逻辑是,如果名字的第一个单词是titles列表的一个成员的话,就会忽略这个单词,返回后面的first-name部分。否则,我们像之前一样使用第一个元素的话。另一个内建函数member,就会检测,如果第一个参数是列表的一部分,就会将第二个参数传进去。
特殊形式if的语法是这样(if 测试条件 then部分 else部分)。在Lisp中有很多特殊形式是用来做条件测试的;if是本例子中最合适的。If语句的求值顺序是最先求值测试部分。如果为真,那么then部分就会被求值并且作为if语句的值返回。如果测试部分为假,那么else部分就会被求值,之后返回。有一些语言坚持说是if语句的测试部分的值必须是true或者是false,Lisp对待这个问题宽容很多。测试部分求值的结果任何都是合法的。只有值nil被认为是false的值;其他所有的值都看做是true。在下面first-name的定义中,如果第一个元素是titles中的,函数member会返回一个非nil的值(true)。如果不是,就会返回nil(false)。虽然所有的非nil值都被看做是true,按照惯例常量t一般是用来表示真。
(defun first-name (name)
"Select the first name from a name represented as a list."
(if (member (first name) *titles*)
(first-name (rest name))
(first name)))
当我们使用一个新的first-name版本的时候,结果会更好一些。另外,函数的操作会一次性把很多的前缀去掉。
> (mapcar #'first-name names)
(JOHN MALCOLM GRACE SPOT ARISTOTLE A Z LARRY SCARLET)
> (first-name '(Madam Major General Paula Jones)
PAULA
通过追踪first-name的执行过程,我们可以看到程序是如何运行的,都有那些值输入,以及输出了哪些值。特殊形式trace和untrace就是这个用处的。
> (trace first-name)
(FIRST-NAME)
> (first-name '(John Q Public))
(1 ENTER FIRST-NAME: (JOHN Q PUBLIC))
(1 EXIT FIRST-NAME: JOHN)
JOHN
当first-name被调用,按照定义就是单个参数的输入,一个名字,值是(JOHN Q PUBLIC),最终返回的值是JOHN。Trace打印的两行信息是显示函数的输入和输出,之后是Lisp打印的最终结果JOHN。
下一个例子更加复杂一些,函数first-name被调用了四次。第一次,输入的名字是(Madam Major General Paula Jones)。因为第一个元素是Madam,是titles的元素之一,结果就是再一次调用first-name,输入的就是剩下的部分(Major General Paula Jones)。过程反复了两簇,最终输入的名字是(Paula Jones)。Paula不是titles的元素,就成为了first-name的结果,也就是这四次调用的结果。Trace是开启追踪,也可以用untrace来关闭追踪。
> (first-name '(Madam Major General Paula Jones)) =>
(1 ENTER FIRST-NAME: (MADAM MAJOR GENERAL PAULA JONES))
(2 ENTER FIRST-NAME: (MAJOR GENERAL PAULA JONES))
(3 ENTER FIRST-NAME: (GENERAL PAULA JONES))
(4 ENTER FIRST-NAME: (PAULA JONES))
(4 EXIT FIRST-NAME: PAULA)
(3 EXIT FIRST-NAME: PAULA)
(2 EXIT FIRST-NAME: PAULA)
(1 EXIT FIRST-NAME: PAULA)
PAULA
> (untrace first-name) => (FIRST-NAME)
> (first-name '(Mr Blue Jeans)) => BLUE
First-name函数可以被称作是递归的,因为它的函数定义中包含了对自身的调用。第一次接触递归这个概念的程序员或许认为他很神秘。但是递归函数事实上和非递归函数没有区别。任何函数对于给定的输入都会返回正确的值。对于这句话的理解可以拆成两部分来看,一个函数必须返回一个值,函数不能返回任何错误的值。两句话等价于前面的一句话,但是思考起来就更加容易,程序的设计也更加方便。
接下来我说明一下first-name问题的抽象描述,突出一下函数的设计,说明一个事实,递归的解决方案并不以任何方式和Lisp绑定的。
function first-name(name);
if 名字的第一个元素师title的元素
then 搞点复杂的事情包first-name找出来
else 返回第一个元素
这把整个问题剖开成了两个部分。在第二部分中,直接返回答案,并且就是正确答案,我们还没有定义第一部分的该做些什么。但是我们知道答案应该就在第一个元素之后的列表中,我们要做的就是对后面的列表进行操作。这部分就是说再一次调用first-name,即使还没有完成所有的定义。
function first-name(name);
if 名字的第一个元素师title的元素
then 将first-name应用到名字的剩余部分
else 返回第一个元素
现在,first-name的第一部分就是递归的,第二部分仍然没有改变。第二部分会返回正确的值,这是我们确信的,第一部分返回的值只是first-name返回的。所以first-name作为一个整体反悔了正确的答案。因此,对于求名字的答案,我们算是做了一半了,另外一半就是要看返回的一些答案了。但是每一个递归调用都会砍掉第一个元素,在剩下的中寻找,所以对于n个元素的列表至多有n重递归调用。这样函数的正确就完整了。深入学习之后,递归的思想就不是一个令人困惑的谜题,而是一种有价值的思想。

1.7 高阶函数

函数步进可以被调用或者操作参数,还可以像其他对象一样被操作。接受一个函数作为参数的函数被称作高阶函数。之前的mapcar就是一例。为了显示高阶函数的编程风格,我们来定义一个新的函数叫做mappend。接受两个参数,一个函数一个列表。mappend将函数定位到每一个列表的元素上,然后将他们追加在一个结果中。第一个定义使用了apply函数,会把指定的函数应用到参数列表中。
(defun mappend (fn the-list)
"Apply fn to each element of list and append the results."
(apply #'append (mapcar fn the-list)))
现在我们尝试理解apply和mappend是如何工作的。第一个例子是将加函数应用到四个参数上。
> (apply #'+ '(123 4)) => 10
下一个例子是将append应用在两个参数的列表上,每一个参数都是列表,如果参数不是列表,就会报错。
> (a pp 1 y #' append ' ((1 2 3) (a b c))) => (1 2 3 ABC)
我们现在定义一个新函数self-and-double,引用到多个参数上。
> (defun self-and-double (x) (list x (+ x x)))
> (self-and-double 3)=> (3 6)
> (apply #'self-and-double '(3)) => (3 6)
如果我们给self-and-double输入超过一个参数,或者输入不是数字的话,就会报错。对表达式(self-and-double 3 4)或者(self-and-double 'Kim)求值就会报错。现在,让我们回到定位函数。
> (mapcar #'self-and-double '(1 10 300)) => ((1 2) (10 20) (300 600))
> (mappend #'self-and-double '(1 10 300)) => (1 2 10 20 300 600)
给mapcar传递一个三个参数的列表,结果总是三个元素的列表。每一个值及时调用函数产生的结果。相对的,mappend被调用的时候,返回的是一个大列表,就相当于mapcar的所有都是在一个追加列表中。如果给mappend传递的函数不是返回列表的话,会报错,原因是append要求他的参数是列表。
现在考虑这样一个问题:给定一个列表,返回的列表是由原始列表中的数字和这些数字的负数组成的列表。例如输入是(testing 1 2 3 test)就返回 (1 -1 2 -2 3 -3)。这个问题用mappend做组件很容易就解决了。
(defun numbers-and-negations (input)
"Given a list, return only the numbers and their negations."
(mappend #'number-and-negation input))
(defun number-and-negation (x)
"If x is a number, return a list of x and -x."
(if (numberp x)
(list x (- x))
nil ) )
> (numbers-and-negations '(testing 1 2 3 test)) => (1 -1 2 -2 3 -3)
下面mappend的可选定义并没有使用mapcar,代替的是一次构建一个元素。
(defun mappend (fn the-list)
"Apply fn to each element of list and append the results."
(if (null the-list)
nil
(append (funcall fn (first the-list))
(mappend fn (rest the-list)))))
Funcall类似于apply,他接受函数作为第一个参数,然后将函数应用到后面的参数中,但是在funcall中,后面的参数是独立列出的。
> (funcall #' + 2 3) => 5
> (apply #'+ '(2 3)) => 5
> (funcall #' + '(2 3)) => Error: (23) is not a number.
这几个表达式分别等价于(+ 2 3), (+ 2 3),和(+ ’(2 3))。
到现在为止用的函数,要么是Common Lisp预定义好的,要么是defun引入的,都是有名字的,没有名字就可以使用的函数也是有的,需要介绍特殊句法lambda。
Lambda这个名字来自于数学家阿隆佐邱奇发明的函数表达法。Lisp一般是倾向于使用简洁的希腊字母,但是lambda是一个例外。更加贴切的名字应该是叫make-function。Lambda表达式是从Russell和Whitehead合著的《数学原理》中的表达法导出得来,他在绑定变量的上面加上了一个标记符号。邱奇想要的是一个一维的字符串,所以他将标记符移动到了表达式的前面”x(x + x),在标记的下面什么都没有是比较搞笑,所以邱奇就把这个符号替换成了lambda字母,但是lambda的大写希腊字母很容易和其他字母搞混,一般是用小写的lambda放在前面。约翰麦卡锡曾经是邱奇教授在普林斯顿的学生,所以当麦卡锡在1958年发明Lisp的时候,他继承了lambda标记法。在键盘上没有希腊字母的时代,麦卡锡就用lambda来表示,一直延续到了今天。一般来说一个lambda表达式是这样子的:
(lambda (参数 ...) 主体 ...)
一个lambda表达式是一个函数的非原子式名字,就像append是一个内建函数的原子式名字。这样子的匿名函数,第一次使用就是在调用的位置进行调用,但是如果我们想要在函数中调用的话,还是需要加上#’符号。
> (lambda (x) (+ x 2)) 4) => 6
> (funcall #'(lambda (x) (+ x 2)) 4) => 6
为了理解两者之间的差别,我们必须要搞清楚表达式是究竟如何求值的。求值的正常规则是这样:对所有的符号求值为所指向的对象。所以在(+ x 2)中的x求值的结果是名字为x的变量的值。列表的求值是两种方式之一,如果列表中的第一个元素是特殊形式操作符,之后的列表就根据特殊形式的语法规则进行求值。否则,列表就解释成一个函数调用。作为函数,第一个元素以一种独特的方式求值。这就意味着,他就是一个符号或者是一个lambda表达式。无论哪一种,第一个元素的名字的函数都会对后面的参数求值后的结果操作。这些值是有正常求值规则决定的。如果我们想要指向一个除了第一个元素的调用意外的位置,就需要使用#’,否则表达式就会用正常的求值规则,也不会被看做是函数。例如:
> append => Error: APPEND is not a bound variable
> (1 ambda (x) (+ x 2)) => Error: LAMBDA is not a function
还有一些正确使用函数的例子:
> (mapcar #'(lambda (x) (+ x x)
'(1 2 3 4 5)) =>
(2 4 6 8 10)
¬ > (mappend #'(lambda (1) (list 1 (reverse 1)))
'((1 2 3) (a b c))) =>
(1 2 3) (3 2 1) (A B C) (C B A))
有时候使用其他编程语言的程序员还不能使用lambda表达式来看问题。Lambda表达式很有用的理由有两个:第一,对于一些边角料一般的程序没必要专门分配一个名字。比如对于表达式(a+b )*( c+d),在程序中需要,但是没有必要一定要加上一个temp或者temp2这样的名字来存储。使用lambda就可以让代码更贱清楚一些,不用再找一个名字了。
第二点更重要的是,lambda表达式使得在运行时创建函数称为可能。这种强大的技术在大部分的编程语言中是不可能实现的。这些运行是函数,被称为闭包,将会在3.16小节介绍。

1.8 其他数据类型

到现在为止,我们只见到了四种Lisp对象:数字,符号,列表和函数。Lisp实际上定义了25中不同类型的对象:向量,数组,结构,字符,流,哈希表,等等。这里我们再引入一个,字符串。你会在之后看到,字符串和数字一样,是求值为自身的。字符串主要用在打印信息,符号则是主要用在与其他对象的关系,变量命名。字符串的打印形式是在两边都会有一个双引号。
> "a string" => "a string"
> (length "a string") => 8
> (length "") => 0

1.9 总结:Lisp的求值规则

现在我们总结一下Lisp的求值规则:
每一个表达式,不是列表就是原子
每一个待求值的列表,不是特殊形式表达式就是一个函数应用
特殊形式表达式的定义,就是第一个元素是特殊形式操作符的列表。表达式的求值遵循的是怪异的求值规则。例如,setf的求之规则就是:第二个参数正常求值,将第一个参数赋值,然后返回那个值。Defun的规则是定义一个西函数,返回函数的名字。Quote的规则是返回不求值的第一个参数。标记’x实际上就是quote函数的缩写。相似的,标记#’f是特殊形式表达式(function f)的缩写
'John = (quote John) => JOHN
(setf p 'John) => JOHN
(defun twice (x) (+ x x)) => TWICE
(if (= 2 3) (error) (+ 5 6) => 11
函数应用的求值规则:首先对列表第一个元素之外的所有参数求值,之后找到第一个元素对应的函数,应用在参数上。
(+ 2 3) => 5
(- (+ 90 9) (+ 50 5 (length '(Pat Kim)))) => 42
请注意如果'(Pat Kim)没有引号的话,会被当做函数应用来处理。
每一个原子,不是非符号就是符号。(这里相当于废话,原文是a symbol or an nonsymbol,我的理解是符号是原子,不能进行破拆得对象也就是具有原子的特性。比如字符串,任何求值为自身的数字,字符串)
符号被求值出来的值就是变量名最近被赋值的那个值。符号由字母组成,可能有数字,极少会有标记符号。为了避免歧义,我们使用的符号大部分是字母字符组成,只有少数例外。例外比如是全局变量,是用星号包裹的。
names
p
*print-pretty*
非符号原子是求值为自身。现在为止,我们所知的只有数字和字符串是非符号原子。数字是由数组成的,可能还有十进制点和符号。另外的一些支持是科学记数法,分数,负数,还有不同进制的数字。字符串是由双引号包裹的字符。
42 ¬=> 42
-273.15 ¬=> -273.15
"a string" => "a string"
还有一些小细节会让定义变得复杂一些,但是这里这样的定义足够了。
对于Lisp初学者来说,引起困惑的其中一点就是读取表达式和求值表达式的区别。初学者在输入的时候经常想像:
> ( + (* 3 4 ) (* 5 6))
会这样想像,首先机器读取(+,知道是加函数,之后就会读取(* 3 4)计算出值是12,之后读取(* 5 6)计算出值是30,最后计算出值是42。事实上,机器真正的行为是一次性读取了整个表达式。列表(+ (* 3 4) (* 5 6))。在被读取之后,系统才开始求值。求职的过程可以用解释器看到列表显示,或者用编译器来翻译成机器码指令,之后执行。
之前我们的描述不是很准确,说,数是由数字组成,可能还会有十进制小数点和符号。准确的说法应该是这样,一个数字的打印形式,是函数读取的形式也是函数打印的形式,是由数字组成,可能还有十进制小数点和符号。数字在机器内部的行书根据机器不同而不同,但你可以确信的是在内存的特定位置会有一个bit位的模式存在,内部的数字自然是不包含打印的字符的十进制形式。相似的,字符串的打印形式是用双引号括起来;它的内部形式是一个字符向量。
初学者对于表达式的读取和求值可能已有了比较好的理解,但是对于表达式求值的效率了解仍然不多。有一次一个学生使用了一个单字母的符号作为变量名,因为他觉得计算机检索一个字母会比检索多个字母快一些。事实上,短的名字在读取的过程是会快一些,但是在求值中是没有区别的。每一根变量,不管名字是什么样的,都仅仅是一个内存位置,内存的访问和变量名字是无关的。

1.10 是什么造就了Lisp的与众不同?

是什么让Lisp区别于其他编程语言?为什么Lisp是一个适用于AI的编程语言?下面主要是说了八点重要的因素:
对列表的内建支持
自动存储管理
动态类型
头等函数
统一的语法
交互式环境
可扩展性
历史悠久
总的来说,这些因素可以让程序猿慢慢做决定。举个例子,对于变量的命名来说,我么可以使用内建的列表函数来构造和操作名字,而不用显式地决定变量名字的展现是什么样子。如果我们决定改变展现,回头更改程序的一部分是很容易的,其他部分不用修改。
这种延迟决定的能力,或者更加精确点说,做出临时的非绑定的决定的能力,通常是一件好事,因为这就意味着一些不恰当的细节可以被忽视了。当然延迟做决定的负面影响也是有的。首先,我们给编译器的信息越少,就会有更大的几率产生很多低效率的代码。第二,我们告诉编译器越少,编译器给出的前后不一致或者警告就会越少。错误的发生可能会延迟到运行状态。我们会深入的考虑每一个因素,权衡每一点的利弊:

列表的内建支持

列表,是一个非常丰富多彩的结构,在任何语言中都会有列表的实现,Lisp是让他变的更加易用。很多AI应用程序包含了常态可变大小的列表,定长的数据结构,类似于向量是比较难用的。
再起的Lisp版本将列表作为他们唯一的内聚数据结构。Common Lisp也提供了其他的数据结构,因为列表并不总是最高效的选择。

自动内存管理

不需要关心内存位置的细节,试下自动内存管理可以给程序员省下很多精力,也会让函数式编程更加方便。其他的语言则会给程序员一些选择。变量的内存位置是在栈中,意思是过程开始的时候被分配,过程结束就会销毁,这是比较有效率的方式,但是却排除了函数会返回复杂值的可能性。另一个选择,就是显式地分配内存并且手动释放。也可以用函数式编程但是可能会导致错误发生。
举个例子,计算表达式a * (b + c),abc都是数字。其实用任何语言都能实现这个计算,下面是Pascal和Lisp的代码:
/* Pascal */
a * ( b + c )
;;;Lisp
( * a ( + b c))
他们之间唯一的区别就是Pascal使用的是中缀表达式,而Lisp使用前缀表达式。现在,我们把abc都替换成矩阵。假设我们已有矩阵的加法和乘法过程。在Lisp中表达式形式是和上面完全一样的;只有函数名变化了。在Pascal中,我们需要声明临时变量来保存栈中的中间结果,之后用一系列的过程调用替换函数表达式:
/* Pascal */
var temp, result: matrix;
add(b,c,temp);
mult(a,temp,result);
return(result);
;;;Lisp
(mult a (add b c))
用Pascal实现的另一种方式是在堆内存中分配矩阵的空间。之后就可以和Lisp使用一样的表达式了。然而,实践中却不会这么做,因为这需要显式地管理内存。
/* Pascal */
var a,b,c,x,y: matrix;
x := add(b,c);
y := mult(a,x);
free(x);
return y;
一般来说,对于Pascal程序员,选择销毁哪一个数据结构是很艰难的决定,如果搞错了,就很容易造成内存溢出。更糟的话,程序员如果销毁了还在使用的结构,之后对这块内存的再分配就会报错。Lisp自动分配和销毁结构,所以都没有这些问题。

动态类型

Lisp程序员不需要给出变量的类型声明,因为语言本身会在运行时确定每一个对象的类型,而不是在编译时指定。这会让Lisp程序更简短,开发更快,也可以使得原来并没有某项功能的函数,扩展成适应特定对象的函数。在Pascal中,我们可以写一个函数来对一百个整数的数组进行排序,但是我们不能将这个过程用在200个整数的数组或者100个字符串的排序。在lisp中,一个函数适应所有对象。
理解这个优点的一个方式就是看看在其他语言中,实现这灵活性有多难。Pascal是不可能的;事实上转为修正Pascal的问题而发明的Modula语言也是不可能。Ada语言的设计考虑了灵活的通用函数,但是Ada的方案还不是很理想:他用过长的篇幅来实现Lisp中的简短功能,并且Ada的编译器也是过于冗杂。(反正就是黑其他语言)。
换句话说,动态类型的缺点,就是一些错误会待到运行时才会被探测出来。强类型语言的一个巨大的好处就是在编译时错误就可以检测出来。强类型语言的一大失败之处也是只能找出一小部分的错误。编译器可以告诉你诸如将字符串误传入数字函数中这样的问题,但是他不会发现,将奇数传入需要偶数的函数这类问题。

头等函数

头等,头等对象的意思就是可以再任何地方使用,以操作其他对象相同的方式进行操作的对象。在Pascal或者C中,函数可以作为参数传递给另一个函数,但是他们不是头等的,因为在运行的时候不可以创建一个新的函数,也不可以创建一个没有名字的匿名函数。在Lisp中我们可以使用lambda表达式,这个会在3.16小节解释。

统一的语法

Lisp程序的语法简单易学,打字错误易纠正。另外可以写程序操作其他程序或者定义一种全新的语言-这是一个强大的技术。简单的语法使得Lisp易于分析。要使用的编辑器应该支持自动缩进还有括号匹配。不然看Lisp程序就头大了。
当然了,有些人是反对一切都诉诸括号。对于反对有两个回应,第一,换个角度想想看,如果Lisp是用所谓传统的语法,括号对就会用什么来替代呢?在算术或者逻辑运算的条件下,就需要一个隐式的运算符优先级,在控制流的情况下就需要一个开始结束的标记存在。但是这两个都不一定是优点。隐式的优先级制度是恶名昭彰的错误易发地带,而开始结束标记仅仅是给代码徒增空间,没有实际的意义。很多语言都从开始结束标记脱离了:C语言就是用了花括号{},效果和括号一样。一些现代的函数式语言(比如Haskell)就是用横向空格符号,没有任何显式的组织。
第二,很多Lisp程序员都考虑过,用预处理器将传统的语法翻译成Lisp语法。但是没有一个最终流行的。并不是程序员们觉得Lisp的括号能忍,而是找到了括号的好,相信你用过一段也会觉得好的。
还有一点很重要的就是Lisp数据的语法和程序的语法是一样的。显然,将数据转化成程序是非常方便的。更好的是,直接省掉了让通用函数处理I/O的时间,Lisp函数的读取和打印都是自动处理任何的列表,结构,字符串或者数字。这样开发过程中单个函数的测试就很平常了。在传统语言,比如C或者Pascal,你会写一个特定目的的函数来读取打印每一个对象,以做到调试的目的,也有一些特殊目的的驱动来做这件事情。由于又花时间又容易出错,往往是避免测试的。Lisp的特性使得测试更加容易,开发更加快捷。

交互式环境

传统上来说,一个程序员会先写一个完整的程序,编译,修正编译器报出的错误,之后运行调试。这个过程就是批处理交互。对于很庞大的程序,编译器的等待时间就占用了调试的很大一部分。在Lisp中一般是一次写一个小函数,马上就从求值系统中获得反馈。这种方式就是交互式环境。只有更改过的函数需要重新编译,所以等待的时间大大缩短了。另外,Lisp程序员可以在任何时候输入任意的表达式调试。
请注意,交互式和批处理语言的概念和解释型语言与编译型语言是不一样的。这两者经常被搞混,Lisp的价值在于是一门交互式语言。实际上有经验的Common Lisp程序员倾向于使用编译器。重点在于交互式而不是解释型。
交互式环境的概念变得流行起来,甚至传统的语言,C和Pascal都开始提供交互式版本,所以这已经不是Lisp独有的优势了。一个C解释器可能会允许程序员输入表达式,之后马上求值反馈,但是不会允许写函数,比如,便利符号表,找出用户定义的函数,然后打印信息。在C中,甚至是解释的C中,符号表都仅仅是为了适应解释器的无用发明,程序结束就销毁。在Lisp中,符号表是头等对象,都是由函数来进行访问维护的。
Common Lisp提供了一个极其丰富的工具集,包括了超过700个内建函数(ANSI Common Lisp提供了900多个)。因此,写程序更多的是对已有的代码进行堆砌,写一些原创的新代码会少一些。除开标准函数,Common Lisp的实现一般会提供交互式编译器扩展,调试器和图形窗口。

可扩展性

在Lisp发明的1958年,没有人预见到,在过去的几十年间,编程语言设计和编程理论会有如此巨大的发展。早期的其他语言已经退出历史的舞台,取而代之的是基于更新观念的语言。但是Lisp延续了下来,因为它本身的适应能力。Lisp是可扩展的,面对新的流行特性,他在不断的演进自己。
扩展语言最简便的方法就是宏,在语言架构中,case和if-then-else结构就是宏来实现的。但是Lisp的灵活性远不止如此,全新的编程风格也能简单实现。很多AI应用程序是基于规则编程的概念上的,其他的编程风格,比如面向对象,也用Common Lisp对象系统(CLOS)来实现了,宏幂函数数据类型的集合已经整合进了ANSI Common Lisp中。
下面的例子展示了Lisp已经在前言走了多远:
(PROG (LIST DEPTH TEMP RESTLIST)
(SETQ RESTLIST (LIST (CONS (READ) 0)) )
A (COND
((NOT RESTLIST) (RETURN ‘DONE))
(T (SETQ LIST (UNCONS (UNCONS RESTLIST
RESTLIST)DEPTH))
(COND ((ATOM LIST)
(MAPC ‘PRIN1 (LIST ‘”ATOM:” LIST ‘”,” ‘DEPTH DEPTH))
(TERPRI))
(T (SETQ TEMP (UNCONS LIST LIST))
(COND (LIST
(SETQ RESTLIST (CONS(CONS LIST DEPTH) RESTLIST))))
(ADD1 DEPTH)) RESTLIST))
))))
(GO A))
请注意,这里有一个现在已经被摒弃了的go语句,程序也缺少足够的缩进(附注,其实是markdown的代码语法不支持缩进,我尝试了很多代码的表现,图片啦,加粗啦,最后这个反引号算是效果还不错了,只是没办法行内缩进。)用递归实现的版本如下:
(PROG NIL (
(LABEL ATOMPRINT (LAMBDA (RESTLIST)
(COND ((NOT RESTLIST) (RETURN ‘DONE))
((ATOM (CAAR RESTLIST)) (MAPC ‘PRIN1
(LIST ‘”ATOM:” (CAAR RESTLIST)
‘”,” ‘DEPTH (CDAR RESTLIST)))
(TERPRI)
(ATOMPRINT (CDR RESTLIST)))
(T (ATOMPRINT (GRAFT
(LIST (CONS (CAAAR RESTLIST) (ADD1 (CDAR RESTLIST))))
(AND (CDAAR RESTLIST) (LIST (CONS (CDAAR RESTLIST)
(CDAR RESTLIST))))
(CDR RESTLIST )))))))
(LIST (CONS (READ ) 0))))
这两个版本都很难阅读,使用现代的眼光来看(文本编辑器和自动缩进),更简单的版本也可以实现。
(defun atomprint (exp &optional (depth 0))
“print each atom in exp. Along with its depth of nesting.”
(if (atom exp)
(format t “~&ATOM: ~a,DEPTH ~d” exp depth)
(dolist (element exp)
Atomprint element (+ depth 1))))

1.11 练习题

【m】1.1 定义一个last-name来处理"Rex Morgan MD," "Morton Downey, Jr.,"
【m】1.2 定义一个取幂的函数,将数字乘n次方,例如(power 3 2)就是3的2次方,9。
Write a function that counts the number of atoms in an expression.
For example: (count-atoms '(a (b) c)) = 3. Notice that there is something of an
ambiguity in this: should (a nil c) count as three atoms, or as two, because it is
equivalent to (a () c)?
【m】1.3 写一个函数来计算表达式中原子的数量。例如,(count-atoms '(a (b) c)) = 3。有一点需要辨明,表达式(a nil c)的原子数量是算作三个还是两个?因为这个表达式等于(a () c)。
【m】1.4 写一个函数来计算一个表达式中出现的另一个表达式的次数。例如:
(count- anywhere 'a '( a (a) b) a)) => 3
【m】1.5 写一个函数来计算两个数字序列的点积(笛卡尔乘积)。
(dot-product '(10 20) '(3 4) = 10 x 3 + 20 x 4 = 110

1.12 习题答案
1.2
exanswer1.2.JPG
1.3
exanswer1.3.JPG
1.4
exanswer1.4.JPG
1.5
exanswer1.5.JPG
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容