Featured image of post ANSI Common Lisp 第二章

ANSI Common Lisp 第二章

介绍Lisp语言的基础概念,包括表达式、求值规则、数据类型、函数定义等核心内容。

📚 返回 Paul Graham 文章目录

ANSI Common Lisp 第二章

(本文摘自Paul Graham的ANSI Common Lisp第二章。
版权所有 1995,Prentice-Hall。)

欢迎使用Lisp

本章旨在让你尽快开始编程。到本章结束时,你将掌握足够的Common Lisp知识来开始编写程序。

2.1 形式

对于Lisp来说,通过使用来学习它特别重要,因为Lisp是一种交互式语言。任何Lisp系统都会包含一个称为顶层(toplevel)的交互式前端。你在顶层输入Lisp表达式,系统会显示它们的值。

Lisp通常会显示一个提示符来告诉你它在等待你输入。许多Common Lisp的实现使用>作为顶层提示符。这就是我们在这里要使用的。

最简单的Lisp表达式之一是整数。如果我们在提示符后输入1:

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,要么是列表,由零个或多个用括号括起来的表达式组成。这些都是有效的Lisp表达式:

    2   (+ 2 3)   (+ 2 3 4)   (/ (- 7 1) (- 4 2))

正如我们将看到的,所有Lisp代码都采用这种形式。像C这样的语言有更复杂的语法:算术表达式使用中缀表示法;函数调用使用一种前缀表示法,参数用逗号分隔;表达式用分号分隔;代码块用花括号分隔。在Lisp中,我们使用单一的表示法来表达所有这些概念。

2.2 求值

在上一节中,我们在顶层输入表达式,Lisp显示它们的值。在本节中,我们仔细看看表达式是如何求值的。

在Lisp中,+是一个函数,像(+ 2 3)这样的表达式是一个函数调用。当Lisp求值一个函数调用时,它分两步进行:

  1. 首先从左到右求值参数。在这种情况下,每个参数都求值为自身,所以参数的值分别是2和3。

  2. 参数的值被传递给由运算符命名的函数。在这种情况下,它是+函数,它返回5。

如果任何参数本身是函数调用,它们按照相同的规则求值。所以当(/ (- 7 1) (- 4 2))被求值时,发生以下情况:

  1. Lisp求值(- 7 1):7求值为7,1求值为1。这些值被传递给-函数,它返回6。

  2. Lisp求值(- 4 2):4求值为4,2求值为2。这些值被传递给-函数,它返回2。

  3. 值6和2被发送给/函数,它返回3。

Common Lisp中的运算符并不都是函数,但大多数是。函数调用总是这样求值的。参数从左到右求值,它们的值被传递给函数,函数返回整个表达式的值。这被称为Common Lisp的求值规则。

一个不遵循Common Lisp求值规则的运算符是quote。quote运算符是一个特殊运算符,这意味着它有自己独特的求值规则。这个规则是:什么都不做。quote运算符接受一个参数,并原样返回它:

(quote (+ 3 5)) (+ 3 5)

为了方便,Common Lisp定义’作为quote的缩写。你可以通过在任意表达式前面加上’来获得调用quote的效果:

‘(+ 3 5) (+ 3 5)

使用缩写比写出整个quote表达式要常见得多。

Lisp提供quote作为保护表达式不被求值的一种方式。下一节将解释这种保护如何有用。


摆脱困境

如果你输入Lisp无法理解的内容,它会显示错误信息并把你放入一个称为断点循环(break loop)的顶层版本。断点循环给有经验的程序员一个机会来找出是什么导致了错误,但最初你在断点循环中唯一想做的就是摆脱它。要回到顶层,你必须输入的内容取决于你的Common Lisp实现。在这个假设的实现中,:abort可以做到:

(/ 1 0) Error: Division by zero. Options: :abort, :backtrace

:abort

附录A展示了如何调试Lisp程序,并给出了一些最常见错误的例子。

2.3 数据

Lisp提供了我们在大多数其他语言中找到的所有数据类型,以及一些我们没有的类型。我们已经使用过的一种数据类型是整数,它写成一系列数字:256。Lisp与大多数其他语言共有的另一种数据类型是字符串,它表示为用双引号括起来的一系列字符:“ora et labora”。整数和字符串都求值为自身。

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章才会讨论,但即使在这个阶段,理解表达式和列表之间的关系也很重要,即使只是为了避免被它迷惑。这就是为什么我们需要引号。如果一个列表被引用,求值会返回列表本身;如果它没有被引用,列表会被当作代码处理,求值会返回它的值:

(list ‘(+ 2 1) (+ 2 1)) ((+ 2 1) 3)

这里第一个参数被引用,所以产生一个列表。第二个参数没有被引用,被当作函数调用处理,产生一个数字。

在Common Lisp中,有两种表示空列表的方式。你可以把它表示为中间没有内容的括号对,或者你可以使用符号nil。你用哪种方式写空列表并不重要,但它会显示为nil:

() NIL nil NIL

你不必引用nil(虽然这样做也无妨),因为nil求值为自身。

2.4 列表操作

cons函数用于构建列表。如果它的第二个参数是一个列表,它返回一个新列表,第一个参数被添加到前面:

(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

2.5 真值

在Common Lisp中,符号t是真理的默认表示。像nil一样,t求值为自身。listp函数如果它的参数是一个列表就返回真:

(listp ‘(a b c)) T

一个返回值旨在被解释为真或假的函数被称为谓词。Common Lisp谓词通常有以p结尾的名字。

Common Lisp中的假由nil(空列表)表示。如果我们给listp一个不是列表的参数,它返回nil:

(listp 27) NIL

因为nil在Common Lisp中扮演两个角色,null函数(它对空列表返回真)

(null nil) T

和not函数(如果它的参数是假就返回真)

(not nil) T

做完全相同的事情。

Common Lisp中最简单的条件语句是if。它通常接受三个参数:一个测试表达式,一个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章解释了如何写你自己的宏。

2.6 函数

你可以用defun定义新函数。它通常接受三个或更多参数:一个名字,一个参数列表,和一个或多个将构成函数体的表达式。这是我们如何定义third:

(defun our-third (x) (car (cdr (cdr x)))) OUR-THIRD

第一个参数说这个函数的名字将是our-third。第二个参数,列表(x),说这个函数将接受恰好一个参数:x。以这种方式用作占位符的符号被称为变量。当变量代表函数的参数时,如x,它也被称为参数。

定义的其余部分,(car (cdr (cdr x))),被称为函数体。它告诉Lisp它必须做什么来计算函数的返回值。所以对our-third的调用返回(car (cdr (cdr x))),无论我们给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

Lisp不区分程序、过程和函数。函数做所有事情(实际上,构成了语言本身的大部分)。如果你想把你的一个函数看作是主函数,你可以,但你通常能够从顶层调用任何函数。在其他方面,这意味着你能够在编写程序时逐段测试它们。

2.7 递归

我们在上一节定义的函数调用其他函数来为它们做一些工作。例如,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是lst的第一个元素,它就是成员。

  3. 否则obj只有当它是lst的其余部分的成员时才是lst的成员。

当你想要理解递归函数如何工作时,把它翻译成这种描述会很有帮助。

许多人最初觉得递归难以理解。很多困难来自于对函数使用错误的比喻。有一种倾向把函数看作某种机器。原材料作为参数到达;一些工作被外包给其他函数;最后成品被组装并作为返回值发出。如果我们用这个比喻来理解函数,递归就变成了一个悖论。一台机器怎么能把工作外包给自己?它已经忙得不可开交了。

对函数更好的比喻是把它看作一个过程。递归在过程中是自然的。我们在日常生活中经常看到递归过程。例如,假设一个历史学家对欧洲历史上的人口变化感兴趣。检查文档的过程可能是这样的:

  1. 获取文档的副本。

  2. 寻找与人口变化相关的信息。

  3. 如果文档提到任何其他可能有用的文档,检查它们。

这个过程很容易理解,但它是递归的,因为第三步可能涉及一次或多次应用相同的过程。

所以不要把our-member看作一个测试某物是否在列表中的机器。相反,把它看作是确定某物是否在列表中的规则。如果我们用这种方式思考函数,递归的悖论就消失了。[2]

2.8 阅读Lisp

上一节中定义的伪member以五个右括号结束。更复杂的函数定义可能以七个或八个结束。刚开始学习Lisp的人看到这么多括号会感到气馁。如何阅读,更不用说写这样的代码?如何看出哪个括号匹配哪个?

答案是,你不必这样做。Lisp程序员通过缩进来阅读和写代码,而不是通过括号。当他们写代码时,他们让文本编辑器显示哪个括号匹配哪个。任何好的编辑器,特别是如果它随Lisp系统一起提供,应该能够做括号匹配。在这样的编辑器中,当你输入一个括号时,编辑器会指示匹配的括号。如果你的编辑器不匹配括号,现在就停下来,弄清楚如何让它匹配,因为如果没有它,实际上不可能写Lisp代码。

[在vi中,你可以用:set sm打开括号匹配。在Emacs中,M-x lisp-mode是获得它的好方法。]

有了好的编辑器,当你写代码时,匹配括号就不再是一个问题。而且因为Lisp缩进有普遍的约定,当你读代码时它也不是问题。因为每个人都使用相同的约定,你可以通过缩进来读代码,忽略括号。

任何Lisp黑客,无论多么有经验,如果our-member的定义看起来像这样,都会发现很难读:

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

实际上,当你在纸上写代码时,这是一个实用的方法。后来,当你输入它时,你可以利用编辑器中的括号匹配。

2.9 输入和输出

到目前为止,我们通过利用顶层隐式地做了输入输出。对于真正的交互式程序,这可能不够。在本节中,我们看看一些用于输入和输出的函数。

Common Lisp中最通用的输出函数是format。它接受两个或更多参数:第一个指示输出要打印到哪里,第二个是一个字符串模板,其余的参数通常是对象,它们的打印表示将被插入到模板中。这是一个典型的例子:

(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要知道的第二件事是它非常强大:read是一个完整的Lisp解析器。它不只是读取字符并作为字符串返回。它解析它读取的内容,并返回产生的Lisp对象。在上面的例子中,它返回了一个数字。

虽然askem的定义很短,但它展示了我们在函数中还没有见过的东西。它的体包含多个表达式。函数的体可以有任意数量的表达式。当函数被调用时,它们会按顺序求值,函数会返回最后一个表达式的值。

在所有前面的章节中,我们坚持使用所谓的"纯"Lisp——即没有副作用。副作用是作为求值表达式的结果发生的对世界状态的某种改变。当我们求值一个纯Lisp表达式如(+ 1 2)时,没有副作用;它只是返回一个值。但当我们调用format时,除了返回值外,它还打印一些内容。这是一种副作用。

当我们写没有副作用的代码时,定义体有多个表达式的函数没有意义。最后一个表达式的值作为函数的值返回,但任何前面的表达式的值都被丢弃。如果这样的表达式没有副作用,你就无法知道Lisp是否真的求值了它们。

2.10 变量

Common Lisp中最常用的运算符之一是let,它允许你引入新的局部变量:

(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

像我们到目前为止看到的变量被称为局部变量。它们只在特定上下文中有效。还有另一种变量,称为全局变量,可以在任何地方可见。

[这里的真正区别是在词法变量和特殊变量之间,但我们直到第6章才需要考虑这一点。]

你可以通过给defparameter一个符号和一个值来创建全局变量:

(defparameter glob 99) GLOB

这样的变量然后在任何地方都可以访问,除了在创建同名新局部变量的表达式中。为了避免这种情况意外发生,通常给全局变量起以星号开始和结束的名字。我们刚才创建的变量的名字会读作"star-glob-star”。

你也可以通过调用defconstant来定义全局常量:

(defconstant limit (+ glob 1))

没有必要给常量起特殊的名字,因为如果有人用同样的名字作为变量会导致错误。如果你想检查某个符号是否是全局变量或常量的名字,使用boundp:

(boundp ‘glob) T

2.11 赋值

在Common Lisp中,最通用的赋值运算符是setf。我们可以用它来对两种变量进行赋值:

(setf glob 98) 98 (let ((n 10)) (setf n 2) n) 2

当setf的第一个参数是一个不是局部变量名字的符号时,它被视为全局变量:

(setf x (list ‘a ‘b ‘c)) (A B C)

也就是说,你可以通过给它们赋值来隐式地创建全局变量。至少在源文件中,使用显式的defparameters是更好的风格。

你可以做的不仅仅是给变量赋值。setf的第一个参数可以是表达式,也可以是变量名。在这种情况下,第二个参数的值被插入到第一个参数引用的位置:

(setf (car x) ’n) N x (N B C)

setf的第一个参数可以是几乎任何引用特定位置的表达式。附录D中标记为"可设置"的所有运算符都是这样的。

2.12 函数式编程

函数式编程意味着通过返回值而不是通过修改事物来编写程序。这是Lisp中的主导范式。大多数内置的Lisp函数是为了它们返回的值而被调用的,而不是为了副作用。

例如,remove函数接受一个对象和一个列表,返回一个新列表,包含除了那个对象之外的所有内容:

(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处理返回值来这样做。要从列表x中删除所有的a,我们说:

(setf x (remove ‘a x))

函数式编程本质上意味着避免setf和类似的东西。乍看之下,可能很难想象这怎么可能,更不用说可取。怎么能只通过返回值来构建程序?

完全不用副作用会很不方便。然而,随着你继续阅读,你可能会惊讶地发现你真正需要的副作用很少。而且你用的副作用越少,你就越好。

函数式编程最重要的优势之一是它允许交互式测试。在纯函数式代码中,你可以在写每个函数时测试它。如果它返回你期望的值,你可以确信它是正确的。这种增加的信心,总的来说,有很大的不同。当你在程序的任何地方做改变时,你都有即时的反馈。这种即时反馈使一种全新的编程风格成为可能,就像电话,与信件相比,使一种新的通信风格成为可能。

2.13 迭代

当我们想重复做某事时,有时使用迭代比递归更自然。迭代的典型情况是生成某种表。这个函数

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

do宏是Common Lisp中的基本迭代运算符。像let一样,do可以创建变量,第一个参数是变量说明的列表。这个列表的每个元素可以是

               (variable initial update)

的形式,其中variable是一个符号,initial和update是表达式。最初每个变量会被设置为对应initial的值;在每次迭代中它会被设置为对应update的值。show-squares中的do只创建一个变量i。在第一次迭代中i会被设置为start的值,在后续迭代中它的值会每次加一。

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。它接受任意数量的表达式,按顺序求值它们,并返回最后一个的值。

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。

这个函数的明显递归版本会是:

(defun our-length (lst) (if (null lst) 0 (+ (our-length (cdr lst)) 1)))

或者,如果列表为空,它的长度是零;否则它是cdr的长度加一。这个版本的our-length更清晰,但因为它不是尾递归的(第13.2节),它不会那么高效。

2.14 函数作为对象

在Lisp中,函数是常规对象,像符号或字符串或列表一样。如果我们给function一个函数的名字,它会返回相关的对象。像quote一样,function是一个特殊运算符,所以我们不必引用参数:

(function +)

这个看起来奇怪的返回值是函数在典型的Common Lisp实现中可能显示的方式。

到目前为止我们只处理了在Lisp显示它们时看起来与我们在输入它们时相同的对象。这个约定不适用于函数。在内部,像+这样的内置函数可能是机器语言代码段。Common Lisp实现可以选择它喜欢的任何外部表示。

就像我们可以用’作为quote的缩写,我们可以用#‘作为function的缩写:

#'+

这个缩写被称为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

通过在lambda表达式前加上sharp-quote,我们得到对应的函数,

(funcall #’(lambda (x) (+ x 100)) 1) 101

在其他方面,这种表示法允许我们使用没有名字的函数。


什么是Lambda?

lambda表达式中的lambda不是一个运算符。它只是一个符号。[3] 在早期的Lisp方言中,它有目的:函数在内部表示为列表,唯一区分函数和普通列表的方法是检查第一个元素是否是符号lambda。


可在以下地址购买:http://www.amazon.com/exec/obidos/ASIN/0133708756

注释

[1] car和cdr的名字来自IBM 704计算机的指令集。car代表"Contents of Address Register”,cdr代表"Contents of Decrement Register”。

[2] 递归的机器比喻来自Abelson和Sussman的Structure and Interpretation of Computer Programs(MIT Press,1985)。

[3] 在早期的Lisp方言中,lambda是一个运算符。在Common Lisp中,它只是一个符号,用于识别lambda表达式。

英文版:sep.turbifycdn.com/ty/cdn/paulgraham/acl2.txt?t=1743250771&|中文版:HiJiangChuan.com/paulgraham/004-Chapter-2-of-Ansi-Common-Lisp

📚 返回 Paul Graham 文章目录

更新记录: