《ANSI Common Lisp》- 第二章:欢迎来到 Lisp【笔记】

本章的目的是让你尽快开始编程。本章结束时,你会掌握足够多的 Common Lisp 知识来开始写程序。

2.1 形式 (Form)

任何 Lisp 系统都含有一个交互式的前端,叫做顶层(toplevel)。你在顶层输入 Lisp 表达式,而系统会显示它们的值。

Lisp 通常会打印一个提示符告诉你,它正在等待你的输入。许多 Common Lisp 的实现用 > 作为顶层提示符。本书也沿用这个符号(在这里我本人的是CL-USER> 或者是 [n]>)。

一个最简单的 Lisp 表达式是整数。如果我们在提示符后面输入 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 4 5)

14

由于操作符可接受不定数量的实参,我们需要用括号来标明表达式的开始与结束。

表达式可以嵌套。即表达式里的实参,可以是另一个复杂的表达式:

>(/ (- 7 1) (- 4 2))

3

2.2 求值 (Evaluation)

上一小节中,我们在顶层输入表达式,然后 Lisp 显示它们的值。在这节里我们深入理解一下表达式是如何被求值的。

在 Lisp 里, + 是函数,然而如 (+ 2 3) 的表达式,是函数调用

当 Lisp 对函数调用求值时,它做下列两个步骤:

    1) 首先从左至右对实参求值。在这个例子当中,实参对自身求值,所以实参的值分别   是 2 跟 3 。

    2) 实参的值传入以操作符命名的函数。在这个例子当中,将 2 跟 3 传给 + 函数,返回 5 。

但不是所有的 Common Lisp 操作符都是函数,不过大部分是。函数调用都是这么求值。由左至右对实参求值,将它们的数值传入函数,来返回整个表达式的值。这称为 Common Lisp 的求值规则。

逃离麻烦

如果你试着输入 Lisp 不能理解的东西,它会打印一个错误讯息,接着带你到一种叫做中断循环(break loop)的顶层。 中断循环给予有经验的程序员一个机会,来找出错误的原因,不过最初你只会想知道如何从中断循环中跳出。 如何返回顶层取决于你所使用的 Common Lisp 实现。在这个假定的实现环境中,输入 :abort 跳出:

>(/ 1 0)

Error:Divisionbyzero

    Options::abort,:backtrace

>>:abort

>

一个不遵守 Common Lisp 求值规则的操作符是 quote 。 quote 是一个特殊的操作符,意味着它自己有一套特别的求值规则。这个规则就是:什么也不做。quote 操作符接受一个实参,并完封不动地返回它。

>(quote (+ 3 5))

(+ 3 5)

为了方便起见,Common Lisp 定义 ' 作为 quote 的缩写。你可以在任何的表达式前,贴上一个 ' ,与调用 quote 是同样的效果:

>'(+ 3 5)

(+ 3 5)

使用缩写 ' 比使用整个 quote 表达式更常见。Lisp 提供 quote 作为一种保护表达式不被求值的方式。下一节将解释为什么这种保护很有用。

2.3 数据 (Data)

Lisp 提供了所有在其他语言找的到的,以及其他语言所找不到的数据类型。一个我们已经使用过的类型是整数(integer),整数用一系列的数字来表示,比如:256 。另一个 Common Lisp 与多数语言有关,并很常见的数据类型是字符串(string),字符串用一系列被双引号包住的字符串表示,比如: "ora etlabora" [3] 。整数与字符串一样,都是对自身求值的。

有两个通常在别的语言所找不到的 Lisp 数据类型是符号(symbol)列表(lists)符号是英语的单词 (words)。无论你怎么输入,通常会被转换为大写:

>'Artichoke

ARTICHOKE

符号(通常)不对自身求值,所以要是想引用符号,应该像上例那样用 ' 引用它。

列表是由被括号包住的零个或多个元素来表示。元素可以是任何类型,包含列表本身。使用列表必须要引用,不然 Lisp 会以为这是个函数调用

>'(my 3 "Sons")

(MY3"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的程序是用列表来表示的。现在了解到列表和表达式的关系是非常重要的,而不是被它们搞混。这也就是为什么我们需要 quote 。如果一个列表被引用了,则求值规则对列表自身来求值;如果没有被引用,则列表被视为是代码,依求值规则对列表求值后,返回它的值。

>(list '(+ 2 1) (+ 2 1))

((+ 2 1) 3)

> (list (+ 2 1) (+ 2 1))

(3 3)

这里第一个实参被引用了,所以产生一个列表。第二个实参没有被引用,视为函数调用,经求值后得到一个数字。

在 Common Lisp 里有两种方法来表示空列表。你可以用一对不包括任何东西的括号来表示,或用符号 nil 来表示空表。你用哪种表示法来表示空表都没关系,但它们都会被显示为 nil :

>()

NIL

>nil

NIL

2.4 列表操作 (List Operations)

用函数 cons 来构造列表。如果传入的第二个实参是列表,则返回由两个实参所构成的新列表,新列表为第一个实参加上第二个实参:

>(cons 'a '(b c d))

(A B C D)

可以通过把新元素建立在空表之上,来构造一个新列表。上一节所看到的函数 list ,不过就是一个把几个元素加到 nil 上的快捷方式:

>(cons 'a (cons 'b nil))

(A B)

>(list 'a 'b)

(A B)

取出列表元素的基本函数是 car 和 cdr 。对列表取 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

2.5 真与假 (Truth)

在 Common Lisp 里,符号 t 是表示逻辑 真 的缺省值。与 nil 相同, t 也是对自身求值的。如果实参是一个列表,则函数 listp 返回 真 :

>(listp '(a b c))

T

函数的返回值将会被解释成逻辑 真 或逻辑 假 时,则称此函数为谓词(predicate)。在 Common Lisp 里,谓词的名字通常以 p 结尾。

逻辑 假 在 Common Lisp 里,用 nil ,即空表来表示。如果我们传给 listp 的实参不是列表,则返回 nil 。

>(listp 27)

NIL

由于 nil 在 Common Lisp 里扮演两个角色,如果实参是一个空表,则函数 null 返回 真 。

>(null nil)

T

而如果实参是逻辑 假 ,则函数 not 返回 真 :

>(not nil)

T

null 与 not 做的是一样的事情。

在 Common Lisp 里,最简单的条件式是 if 。通常接受三个实参:一个 test 表达式,一个 then 表达式和一个 else 表达式。若 test 表达式求值为逻辑 真 ,则对 then 表达式求值,并返回这个值。若 test 表达式求值为逻辑 假 ,则对 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)

    (+ 1 2))

NIL

虽然 t 是逻辑 真 的缺省表示法,任何非 nil 的东西,在逻辑的上下文里通通被视为 真 。

>(if 27 1 2)

1

>(if nil 9 8)

8

逻辑操作符 and 和 or 与条件式类似。两者都接受任意数量的实参,但仅对能影响返回值的几个实参求值。如果所有的实参都为 真 (即非 nil ),那么 and 会返回最后一个实参的值:

>(and t (+ 1 2))

3

如果其中一个实参为 假 ,那之后的所有实参都不会被求值。 or 也是如此,只要碰到一个为 真 的实参,就停止对之后所有的实参求值。

以上这两个操作符称为。宏和特殊的操作符一样,可以绕过一般的求值规则。第十章解释了如何编写你自己的宏。

2.6 函数 (Functions)

你可以用 defun 来定义新函数。通常接受三个以上的实参:一个名字,一组用列表表示的实参,以及一个或多个组成函数体的表达式。我们可能会这样定义third :

>(defun our-third (x)

    (car (cdr (cdr x))))

OUR-THIRD

第一个实参说明此函数的名称将是 our-third 。第二个实参,一个列表 (x) ,说明这个函数会接受一个形参: x 。这样使用的占位符符号叫做变量。当变量代表了传入函数的实参时,如这里的 x ,又被叫做形参

定义的剩余部分, (car (cdr (cdr x))) ,即所谓的函数主体。它告诉 Lisp 该怎么计算此函数的返回值。所以调用一个 our-third 函数,对于我们作为实参传入的任何 x ,会返回 (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)

T

2.7 递归 (Recursion)

上一节我们所定义的函数,调用了别的函数来帮它们做事。比如 sum-greater 调用了 + 和 > 。函数可以调用任何函数,包括自己。自己调用自己的函数是递归的。 Common Lisp 函数 member ,测试某个东西是否为列表的成员。下面是定义成递归函数的简化版:

(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))

NIL

下面是 our-member 的定义对应到英语的描述。为了知道一个对象 obj 是否为列表 lst 的成员,我们:

    1)首先检查 lst 列表是否为空列表。如果是空列表,那 obj 一定不是它的成员,结束。

    2)否则,若 obj 是列表的第一个元素时,则它是列表的成员。

    3)不然只有当 obj 是列表其余部分的元素时,它才是列表的成员。

当你想要了解递归函数是怎么工作时,把它翻成这样的叙述有助于你理解。

2.8 阅读 Lisp (Reading Lisp)

Lisp 程序员用缩排来阅读及编写程序,而不是括号。当他们在写程序时,他们让文字编辑器显示哪个括号该与哪个匹配。任何好的文字编辑器,特别是 Lisp 系统自带的,都应该能做到括号匹配(paren-matching)。在这种编辑器中,当你输入一个括号时,编辑器指出与其匹配的那一个。如果你的编辑器不能匹配括号,别用了,想想如何让它做到,因为没有这个功能,你根本不可能编 Lisp 程序。

2.9 输入输出 (Input and Output)

最普遍的 Common Lisp 输出函数是 format 。接受两个或两个以上的实参,第一个实参决定输出要打印到哪里,第二个实参是字符串模版,而剩余的实参,通常是要插入到字符串模版,用打印表示法(printed representation)所表示的对象。下面是一个典型的例子:

>(formatt "~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 解析器(parser)。不仅是可以读入字符,然后当作字符串返回它们。它解析它所读入的东西,并返回产生出来的 Lisp 对象。在上述的例子,它返回一个数字。

askem 的定义虽然很短,但体现出一些我们在之前的函数没看过的东西。函数主体可以有不只一个表达式。函数主体可以有任意数量的表达式。当函数被调用时,会依序求值,函数会返回最后一个的值。

在之前的每一节中,我们坚持所谓“纯粹的” Lisp ── 即没有副作用的 Lisp 。副作用是指,表达式被求值后,对外部世界的状态做了某些改变。当我们对一个如(+ 1 2) 这样纯粹的 Lisp 表达式求值时,没有产生副作用。它只返回一个值。但当我们调用 format 时,它不仅返回值,还印出了某些东西。这就是一种副作用。

当我们想要写没有副作用的程序时,则定义多个表达式的函数主体就没有意义了。最后一个表达式的值,会被当成函数的返回值,而之前表达式的值都被舍弃了。如果这些表达式没有副作用,你没有任何理由告诉 Lisp ,为什么要去对它们求值。

2.10 变量

let 是一个最常用的 Common Lisp 的操作符之一,它让你引入新的局部变量(local variable):

>(let ((x 1) (y 2))

    (+ x y))

3

一个 let 表达式有两个部分。第一个部分是一组创建新变量的指令,指令的形式为 (variable expression) 。每一个变量会被赋予相对应表达式的值。上述的例子中,我们创造了两个变量, 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

我们已经看过的这些变量都叫做局部变量。它们只在特定的上下文里有效。另外还有一种变量叫做全局变量(global variable),是在任何地方都是可视的。

你可以给 defparameter 传入符号和值,来创建一个全局变量:

>(defparameter *glob* 99)

*GLOB*

全局变量在任何地方都可以存取,除了在定义了相同名字的区域变量的表达式里。为了避免这种情形发生,通常我们在给全局变量命名时,以星号作开始与结束。刚才我们创造的变量可以念作 “星-glob-星” (star-glob-star)。

你也可以用 defconstant 来定义一个全局的常量:

>(defconstant limit (+ *glob* 1))

LIMIT

我们不需要给常量一个独一无二的名字,因为如果有相同名字存在,就会有错误产生 (error)。如果你想要检查某些符号,是否为一个全局变量或常量,使用boundp 函数:

> (boundp '*glob*)

T

2.11 赋值 (Assignment)

在 Common Lisp 里,最普遍的赋值操作符(assignment operator)是 setf 。可以用来给全局或局部变量赋值:

> (setf *glob* 98)

98

> (let ((n 10))

     (setf n 2)

     n)

2

如果 setf 的第一个实参是符号(symbol),且符号不是某个局部变量的名字,则 setf 把这个符号设为全局变量:

> (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)

2.12 函数式编程 (Functional Programming)

函数式编程意味着撰写利用返回值而工作的程序,而不是修改东西。它是 Lisp 的主流范式。大部分 Lisp 的内置函数被调用是为了取得返回值,而不是副作用。

举例来说,函数 remove 接受一个对象和一个列表,返回不含这个对象的新列表:

> (setf lst '(c a r a t))

(C A R A T)

> (remove 'a lst)

(C R T)

为什么不干脆说 remove 从列表里移除一个对象?因为它不是这么做的。原来的表没有被改变。

若你真的想从列表里移除某些东西怎么办?在 Lisp 通常你这么做,把这个列表当作实参,传入某个函数,并使用 setf 来处理返回值。要移除所有在列表 x 的 a,我们可以说:

(setf x (remove 'a x))

函数式编程本质上意味着避免使用如 setf 的函数。起初可能觉得这根本不可能,更遑论去做了。怎么可以只凭返回值来建立程序?

完全不用到副作用是很不方便的。然而,随着你进一步阅读,会惊讶地发现需要用到副作用的地方很少。副作用用得越少,你就更上一层楼。

函数式编程最重要的优点之一是,它允许交互式测试(interactive testing)。在纯函数式的程序里,你可以测试每个你写的函数。如果它返回你预期的值,你可以有信心它是对的。这额外的信心,集结起来,会产生巨大的差别。当你改动了程序里的任何一个地方,会得到即时的改变。而这种即时的改变,使我们有一种新的编程风格。类比于电话与信件,让我们有一种新的通讯方式。

2.13 迭代 (Iteration)

当我们想重复做一些事情时,迭代比递归来得更自然。典型的例子是用迭代来产生某种表格。这个函数:

> (defun show-squares (start end)

  (do ((i start (+ i 1)))

      ((> i end) 'done)

    (format t "~A ~A~%" i (* i i))))

SHOW-SQUARES

列印从 start 到 end 之间的整数的平方:

> (show-squares 2 5)

2 4

3 9

4 16

5 25

DONE

do 宏是 Common Lisp 里最基本的迭代操作符。和 let 类似, do 可以创建变量,而第一个实参是一组变量的规格说明列表。每个元素可以是以下的形式

(variable initial update)

其中 variable 是一个符号, initial 和 update 是表达式。最初每个变量会被赋予 initial 表达式的值;每一次迭代时,会被赋予 update 表达式的值。在 show-squares 函数里, do 只创建了一个变量 i 。第一次迭代时, i 被赋与 start 的值,在接下来的迭代里, i 的值每次增加 1 。

第二个传给 do 的实参可包含一个或多个表达式。第一个表达式用来测试迭代是否结束。在上面的例子中,测试表达式是 (> i end) 。接下来在列表中的表达式会依序被求值,直到迭代结束。而最后一个值会被当作 do 的返回值来返回。所以 show-squares 总是返回 done 。

do 的剩余参数组成了循环的函数体。在每次迭代时,函数体会依序被求值。在每次迭代过程里,变量被更新,检查终止测试条件,接着(若测试失败)求值函数体。

作为对比,以下是递归版本的 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 。 progn 接受任意数量的表达式,依序求值,并返回最后一个表达式的值。

为了处理某些特殊情况, Common Lisp 有更简单的迭代操作符。举例来说,要遍历列表的元素,你可能会使用 dolist 。以下函数返回列表的长度:

>(defun our-length (lst)

    (let ((len 0))

        (dolist (obj lst)

            (setf len (+ len 1)))

        len))

这里 dolist 接受这样形式的实参(variable expression),跟着一个具有表达式的函数主体。函数主体会被求值,而变量相继与表达式所返回的列表元素绑定。因此上面的循环说,对于列表 lst 里的每一个 obj ,递增 len 。很显然这个函数的递归版本是:

(defun our-length (lst)

    (if (null lst)

        0

        (+ (our-length (cdr lst)) 1)))

也就是说,如果列表是空表,则长度为 0 ;否则长度就是对列表取 cdr 的长度加一。递归版本的 our-length 比较易懂,但由于它不是尾递归(tail-recursive)的形式 (见 13.2 节),效率不是那么高。

2.14 函数作为对象 (Functions as Objects)

函数在 Lisp 里,和符号、字符串或列表一样,是稀松平常的对象。如果我们把函数的名字传给 function ,它会返回相关联的对象。和 quote 类似, function是一个特殊操作符,所以我们无需引用(quote)它的实参:

>(function +)

#

这看起来很奇怪的返回值,是在典型的 Common Lisp 实现里,函数可能的打印表示法。

到目前为止,我们仅讨论过,不管是 Lisp 打印它们,还是我们输入它们,看起来都是一样的对象。但这个惯例对函数不适用。一个像是 + 的内置函数 ,在内部可能是一段机器语言代码(machine language code)。每个 Common Lisp 实现,可以选择任何它喜欢的外部表示法(external representation)。

如同我们可以用 ' 作为 quote 的缩写,也可以用 #' 作为 function 的缩写:

> #'+

#

这个缩写称之为升引号(sharp-quote)。

和别种对象类似,可以把函数当作实参传入。有个接受函数作为实参的函数是 apply 。apply 接受一个函数和实参列表,并返回把传入函数应用在实参列表的结果:

>(apply #'+ '(1 2 3))

6

apply 可以接受任意数量的实参,只要最后一个实参是列表即可:

>(apply #'+ 1 2 '(3 4 5))

15

函数 funcall 做的是一样的事情,但不需要把实参包装成列表。

>(funcall #'+ 1 2 3)

6

什么是 lambda ?

lambda 表达式里的 lambda 不是一个操作符。而只是个符号。 在早期的 Lisp 方言里, lambda 存在的原因是:由于函数在内部是用列表来表示, 因此辨别列表与函数的方法,就是检查第一个元素是否为 lambda 。

在 Common Lisp 里,你可以用列表来表达函数, 函数在内部会被表示成独特的函数对象。因此不再需要 lambda 了。 如果需要把函数记为

((x) (+ x 100))

而不是

(lambda (x) (+ x 100))

也是可以的。

但 Lisp 程序员习惯用符号 lambda ,来撰写函数, 因此 Common Lisp 为了传统,而保留了 lambda 。

defun 宏,创建一个函数并给函数命名。但函数不需要有名字,而且我们不需要 defun 来定义他们。和大多数的 Lisp 对象一样,我们可以直接引用函数。

要直接引用整数,我们使用一系列的数字;要直接引用一个函数,我们使用所谓的lambda 表达式。一个 lambda 表达式是一个列表,列表包含符号 lambda ,接着是形参列表,以及由零个或多个表达式所组成的函数体。

下面的 lambda 表达式,表示一个接受两个数字并返回两者之和的函数:

>(lambda (x y)

    (+ x y))

列表 (x y) 是形参列表,跟在它后面的是函数主体。

一个 lambda 表达式可以作为函数名。和普通的函数名称一样, lambda 表达式也可以是函数调用的第一个元素,

> ((lambda (x) (+ x 100)) 1)

101

而通过在 lambda 表达式前面贴上 #' ,我们得到对应的函数,

(funcall #'(lambda (x) (+ x 100))

          1)

lambda 表示法除上述用途以外,还允许我们使用匿名函数。

2.15 类型 (Types)

Lisp 处理类型的方法非常灵活。在很多语言里,变量是有类型的,得声明变量的类型才能使用它。在 Common Lisp 里,数值才有类型,而变量没有。你可以想像每个对象,都贴有一个标明其类型的标签。这种方法叫做显式类型manifest typing)。你不需要声明变量的类型,因为变量可以存放任何类型的对象。

虽然从来不需要声明类型,但出于效率的考量,你可能会想要声明变量的类型。类型声明在第 13.3 节时讨论。

Common Lisp 的内置类型,组成了一个类别的层级。对象总是不止属于一个类型。举例来说,数字 27 的类型,依普遍性的增加排序,依序是 fixnum 、integer 、 rational 、 real 、 number 、 atom 和 t 类型。(数值类型将在第 9 章讨论。)类型 t 是所有类型的基类(supertype)。所以每个对象都属于 t类型。

函数 typep 接受一个对象和一个类型,然后判定对象是否为该类型,是的话就返回真:

>(typep 27 'integer)

T

我们会在遇到各式内置类型时来讨论它们。

2.16 展望 (Looking Forward)


Chapter 2 总结 (Summary)

1)Lisp 是一种交互式语言。如果你在顶层输入一个表达式, Lisp 会显示它的值。

2)Lisp 程序由表达式组成。表达式可以是原子,或一个由操作符跟着零个或多个实参的列表。前序表示法代表操作符可以有任意数量的实参。

3)Common Lisp 函数调用的求值规则: 依序对实参从左至右求值,接着把它们的值传入由操作符表示的函数。 quote 操作符有自己的求值规则,它完封不动地返回实参。

4)除了一般的数据类型, Lisp 还有符号跟列表。由于 Lisp 程序是用列表来表示的,很轻松就能写出能编程的程序。

5)三个基本的列表函数是 cons ,它创建一个列表; car ,它返回列表的第一个元素;以及 cdr ,它返回第一个元素之后的所有东西。

6)在 Common Lisp 里, t 表示逻辑 真 ,而 nil 表示逻辑 假 。在逻辑的上下文里,任何非 nil 的东西都视为 真 。基本的条件式是 if 。 and 与 or 是相似的条件式。

7)Lisp 主要由函数所组成。可以用 defun 来定义新的函数。

8)自己调用自己的函数是递归的。一个递归函数应该要被想成是过程,而不是机器。

9)括号不是问题,因为程序员通过缩排来阅读与编写 Lisp 程序。

10)基本的 I/O 函数是 read ,它包含了一个完整的 Lisp 语法分析器,以及 format ,它通过字符串模板来产生输出。

11)你可以用 let 来创造新的局部变量,用 defparameter 来创造全局变量。

12)赋值操作符是 setf 。它的第一个实参可以是一个表达式。

13)函数式编程代表避免产生副作用,也是 Lisp 的主导思维。

14)基本的迭代操作符是 do 。

15)函数是 Lisp 的对象。可以被当成实参传入,并且可以用 lambda 表达式来表示。

16)在 Lisp 里,是数值才有类型,而不是变量。

笔记:

一、基本的 List 操作函数

1) cons:如果传入的第二个实参是列表,则返回由两个实参所构成的新列表,新列表为第一个实参加上第二个实参。cons 真正所做的事情是,把两个对象结合成一个有两部分的对象,称之为 Cons 对象。概念上来说,一个 Cons 是一对指针;第一个是 car ,第二个是 cdr 。

2)car:对列表取 car 返回第一个元素。

3)cdr:对列表取 cdr 返回第一个元素之后的所有元素

4)listp:符号 t 是表示逻辑 真 的缺省值。与 nil 相同, t 也是对自身求值的。如果实参是一个列表,则函数 listp 返回 真。如果我们传给 listp 的实参不是列表,则返回 nil 。

5)null:如果实参是一个空表,则函数 null 返回 真

6)not:而如果实参是逻辑 假 ,则函数 not 返回 真

7)defun:你可以用 defun 来定义新函数。通常接受三个以上的实参:一个名字,一组用列表表示的实参,以及一个或多个组成函数体的表达式。

8)format:最普遍的 Common Lisp 输出函数是 format 。接受两个或两个以上的实参,第一个实参决定输出要打印到哪里,第二个实参是字符串模版,而剩余的实参,通常是要插入到字符串模版,用打印表示法(printed representation)所表示的对象。

9)read:标准的输入函数是 read

10)numberp:测试它的实参是否为数字

11)boundp:检查某些符号,是否为一个全局变量或常量

12)remove:函数 remove 接受一个对象和一个列表,返回不含这个对象的新列表

13)apply:接受一个函数和实参列表,并返回把传入函数应用在实参列表的结果

14)typep:接受一个对象和一个类型,然后判定对象是否为该类型,是的话就返回真

15)list:创建列表。由于 list 是函数,所以它的实参会被求值。

二、数据类型

1)整数(integer)

2)字符串(string)

3)符号(symbol):符号是英语的单词,无论你怎么输入,通常会被转换为大写。符号(通常)不对自身求值,所以要是想引用符号,应该用 ' 引用它。

4)列表(lists):列表是由被括号包住的零个或多个元素来表示。元素可以是任何类型,包含列表本身。使用列表必须要引用,不然 Lisp 会以为这是个函数调用

三、操作符(好像也可以叫函数)

1)+、- 、*、/、>、< 等

2)quote:quote 是一个特殊的操作符,意味着它自己有一套特别的求值规则。这个规则就是:什么也不做。quote 操作符接受一个实参,并完封不动地返回它。为了方便起见,Common Lisp 定义 ' 作为 quote 的缩写。你可以在任何的表达式前,贴上一个 ' ,与调用 quote 是同样的效果。

3)if:通常接受三个实参:一个 test 表达式,一个 then 表达式和一个 else 表达式。若 test 表达式求值为逻辑 真 ,则对 then 表达式求值,并返回这个值。若 test 表达式求值为逻辑 假 ,则对 else 表达式求值,并返回这个值

与 quote 相同, if 是特殊的操作符。不能用函数来实现,因为实参在函数调用时永远会被求值,而 if 的特点是,只有最后两个实参的其中一个会被求值。 if的最后一个实参是选择性的。如果忽略它的话,缺省值是 nil

4)and:逻辑操作符 and 和 or 与条件式类似。两者都接受任意数量的实参,但仅对能影响返回值的几个实参求值。如果所有的实参都为 真 (即非 nil ),那么 and 会返回最后一个实参的值

5)or:如果其中一个实参为 假 ,那之后的所有实参都不会被求值。 or 也是如此,只要碰到一个为 真 的实参,就停止对之后所有的实参求值。

 and 和 or 这两个操作符称为。宏和特殊的操作符一样,可以绕过一般的求值规则

6)eql:谓词 eql 测试它的两个实参是否相等

7)let:let 是一个最常用的 Common Lisp 的操作符之一,它让你引入新的局部变量(local variable)

8)defparameter:创建一个全局变量

9)defconstant:定义一个全局的常量

10)setf:在 Common Lisp 里,最普遍的赋值操作符(assignment operator)是 setf 。可以用来给全局或局部变量赋值

11)do:do 宏是 Common Lisp 里最基本的迭代操作符。和 let 类似, do 可以创建变量,而第一个实参是一组变量的规格说明列表。

12)function:如果我们把函数的名字传给 function ,它会返回相关联的对象。和 quote 类似, function是一个特殊操作符,所以我们无需引用(quote)它的实参

你可能感兴趣的:(《ANSI Common Lisp》- 第二章:欢迎来到 Lisp【笔记】)