Kika's
Blog
图片简介 | CC BY 4.0 | 换一张

译介:Lisp入门-霍夫斯塔特谈Lisp

2024-11-09

译者按:这是一篇通俗易懂的Lisp入门小品文,原文刊于《科学美国人》,你可以在这里找到原文的副本,以下为此文的中文翻译(LLM+人工细修)

在80年代中期,当我翻阅室友收藏的一系列《科学美国人》旧刊时,我遇到了道格拉斯·霍夫斯塔特写的这篇介绍Lisp的文章。当时我觉得它非常迷人,并且在这里(稍微非法地)提供给新一代Lisper以启发他们。

为了证明了Lisp的永恒性,如果您安装了以下别名,您仍然可以在emacs中运行下面的所有示例:

(defalias 'plus #'+)
(defalias 'quotient #'/)
(defalias 'times #'*)
(defalias 'difference #'-)

Lisp:原子与列表

1983年2月

在之前的专栏中,我经常谈到人工智能领域——寻找编程方式使得计算机能够表现出灵活性、常识、洞察力、创造力、自我意识、幽默感等特质。对AI的探索大约在二十多年前开始,从那时起这一领域已经多次分化,如今成为一个非常活跃且多方面的研究领域。在美国可能有数千人专业从事AI工作,在海外也有类似数量的人从事这项工作。尽管这些工作者对于达到AI的最佳途径存在相当大的意见分歧,但在选择编程语言方面几乎是一致的。大多数AI研究都是用一种叫做“Lisp”的语言进行的。(这个名字并不是一个完整的缩写;它代表“列表处理”。)

为什么大多数AI工作是用Lisp完成的?原因有很多,其中大部分是比较技术性的,但有一个原因非常简单:Lisp是清晰的。或者如玛丽莲·梦露在《七年之痒》中所说,“我认为它就是优雅!”每种计算机语言都有任意特性,而大多数语言实际上都充满了这些特性。然而,有一些语言,例如Lisp和Algol,它们建立在一个看起来就像数学分支一样自然的核心之上。Lisp的核心具有水晶般的纯净,这不仅吸引了美学上的感觉,也使Lisp比大多数其他语言更加灵活。因此,由于Lisp的美丽以及它在这个现代科学重要领域的中心地位,我决定用三篇文章来探讨Lisp的一些基本概念。

Lisp的深厚根基主要在于数理逻辑。数学先驱如索尔夫·斯科勒姆、库尔特·哥德尔和阿隆佐·丘奇在20世纪20年代和30年代为逻辑学贡献了开创性的思想,这些思想在数十年后被融入到Lisp中。真正的计算机编程始于20世纪40年代,但是所谓的“高级”编程语言(Lisp就是其中之一)直到20世纪50年代才出现。最早的列表处理语言不是Lisp而是IPL(“信息处理语言”),这是赫伯特·西蒙、艾伦·纽厄尔和J.C.肖在20世纪50年代中期开发的。在1956年至1958年间,约翰·麦卡锡借鉴了所有这些先前的来源,提出了一种他称之为Lisp的优雅代数列表处理语言。它迅速受到了在他周围MIT新成立的人工智能项目组的年轻人的喜爱,被实现在IBM 704上,传播到了其他的AI团队,感染了他们,并一直流传至今。现在有许多方言存在,但它们都共享那个核心的优雅内核。

现在让我们来看看Lisp实际是如何工作的。Lisp最吸引人的特点之一是它的交互性,这与大多数其他高级语言的非交互性形成了对比。这意味着什么?当你想要用Lisp编程时,你会坐在连接到计算机的终端前并键入单词“lisp”(或类似效果的词语)。接下来你会看到屏幕上出现所谓的“提示符”——一个特定的符号,比如箭头或星号。我喜欢将这个提示符想象成一个特殊的“Lisp精灵”向你问候,深深地鞠躬说:“你的愿望是我的命令——那么,你的下一个愿望是什么?”然后精灵会等待你输入一些东西。这个精灵通常被称为Lisp解释器,它会做你想做的任何事情,但你必须非常小心地准确表达你的愿望,否则可能会导致灾难性的后果。下面显示的是提示符,表明Lisp精灵已准备好执行您的命令:

>

精灵正在询问我们的心愿,所以让我们输入一个简单的表达式:

> (plus 2 2)

然后按回车键。(顺便提一下,在本章及后续两章中,所有的Lisp表达式和单词都会用Helvetica字体打印出来。)即使是非Lisper也可以预料到Lisp精灵将会返回值4。之后它还会打印出一个新的提示符,这样屏幕就会变成如下所示的样子:

> (plus 2 2)
4
>

现在精灵已经准备好执行我们的下一个命令——或者说,更礼貌地说,我们的下一个愿望——如果我们有的话。通过Lisp语句表达的愿望的执行被称为该语句的求值。前面人类与计算机之间的简短交流就体现了Lisp解释器的行为:它读取一条语句,对其进行求值,打印出相应的值,然后发出准备读取新语句的信号。因此,Lisp解释器的核心活动被称为读-求值-打印循环。

正是这个Lisp精灵(Lisp解释器)的存在使得Lisp具有交互性。只要你键入了一个“愿望”——一个完整的语句——到Lisp,你就会立即得到反馈。而要让一堆愿望得以实现的方法就是键入一个,然后请求精灵去执行它,接着再键入另一个,再次请求精灵执行,如此往复。

相比之下,在许多高级计算机语言中,你必须写出整个程序,由大量按照某种指定顺序执行的愿望组成。更糟糕的是,后来的愿望往往强烈依赖于之前愿望的结果——当然,你不能一个接一个地单独尝试它们。这样的程序执行可能会导致很多意想不到的结果,因为这么多的愿望需要完美地结合在一起。如果你在设计愿望清单时犯了哪怕是最小的概念性错误,那么一个彻底的混乱几乎是不可避免的。运行这种类型的程序就像是发射一个未经测试的新太空探测器:你不可能预见到所有可能出错的事情,所以你能做的只是坐下来观察,希望它能正常工作。如果失败了,你就回去纠正失败揭示出的那个问题,然后再试一次。这种笨拙、间接、昂贵的编程方式与直接、互动、一次一个愿望的Lisp风格形成鲜明对比,后者允许增量式的程序开发和调试。这也是Lisp受欢迎的另一个主要原因。

你可以向Lisp精灵输入什么样的愿望供其求值,它又会打印出什么样的东西给你呢?首先,你可以键入一些以相当奇怪的方式表达的算术表达式,例如(times (plus 6 3) (difference 6 3))。这个答案是27,因为(plus 6 3)求值得到9,而(difference 6 3)求值得到3,它们的乘积是27。这种将每个操作放在其操作数左侧的记法是由波兰逻辑学家扬·卢卡谢维奇在计算机出现之前发明的。不幸的是,对于卢卡谢维奇来说,他的名字对于大多数英语使用者来说太难念了,所以这种记法被称为波兰记法。这里有一个简单的波兰记法问题给你,你需要扮演Lisp精灵的角色:

> (quotient (plus 2113) (difference 23 (times 2 (difference 7 (plus 2 2)))))

也许你已经注意到Lisp语句涉及括号。大量的括号是Lisp的一个标志。看到一个以十几个右括号结束的表达式并不罕见!这最初会让许多人感到害怕——然而一旦你习惯了它们特有的外观,Lisp表达式就会变得非常直观,甚至令人感到亲切,尤其是在格式化打印的时候,这意味着遵循一种能够揭示其逻辑结构的仔细缩进方案。本文中的所有展示表达式都已经进行了格式化打印。

Lisp的核心是可操控的结构。Lisp中的所有程序都是通过创建、修改和销毁结构来工作的。结构分为两种类型:原子和复合体,或者通常称为原子和列表。因此,每一个Lisp对象要么是一个原子,要么是一个列表(但不会同时是两者)。唯一的例外是特殊对象nil,它既是原子也是列表。稍后会更多地讨论nil。还有哪些是典型的Lisp原子?这里有几个例子:

_hydrogen, helium, j-s-bach, 1729, 3.14159, pi,
arf, foo, bar, baz, buttons-&-bows_

列表是Lisp中的灵活数据结构。列表大致就是听起来的样子:一组特定顺序的部分。列表的部分通常被称为元素或成员。这些成员可以是什么呢?不出所料,列表可以有原子作为成员。同样容易地,列表也可以包含列表作为成员,那些列表反过来又可以包含其他列表作为成员,以此类推,递归下去。哎呀!我在这里提前使用了这个词。不过没关系。你肯定理解了我的意思,这也会让你为后面出现的技术定义做好准备。

屏幕上打印出来的列表可以通过其括号来识别。在Lisp中,任何由匹配的括号包围的东西都构成一个列表。因此,例如(zonk blee strill (croak flonk))是一个四元素列表,其最后一个元素本身是一个双元素列表。另一个简短的列表是(plus 2 2),这说明Lisp语句本身就是列表。这一点很重要,因为它意味着Lisp精灵可以通过操作列表和原子自己构建新的愿望。因此,一个愿望的对象可以是新愿望的构建及其随后的求值!

接着是空列表——没有任何元素的列表。它是如何书写的呢?你可能会认为一对空括号——()——就可以工作了。确实,它可以工作——但表示空列表还有第二种方式,那就是写作nil。这两种记法是同义的,尽管写作nil比()更为常见。空列表nil是Lisp中的一个关键概念;在列表的世界里,它相当于数字世界中的零。用另一个比喻来形容nil,它就像是所有结构扎根的土壤。但为了理解这句话的含义,你还需要稍等片刻。

原子最常利用的特征是它拥有(或可以被赋予)一个值。有些原子具有永久的值,而另一些则是变量。正如你可能预料的那样,原子1729的值是整数1729,而且这个值是永久不变的。(这里我区分了打印名称或简称是四位数字字符串1729的原子,以及恰好可以用两种不同方法表示两个立方和的永恒柏拉图本质——即数字1729。)nil的值也是永久的,而且它的值就是——nil!只有另一个原子也以其自身作为永久值,那就是特殊的原子t。

除了t、nil以及名字是数字的原子外,原子通常是变量,这意味着你可以为它们赋值并在之后随意更改这些值。如何做到这一点呢?好吧,如果你想给原子pie赋值4,你可以向Lisp精灵输入(setq pie 4)。或者你也可以同样输入(setq pie (plus 2 2))——甚至是(setq pie (plus 1 1 1 1))。在这些情况下的任一种,只要你按下回车键,pie的值就会变成4,并且它会保持不变——至少直到你再次对原子pie执行setq操作为止。

如果原子只能拥有数值作为其值,那么Lisp就不会那么简洁了。幸运的是,原子的值可以设置为任何种类的Lisp对象——任何原子或列表都可以。例如,我们可能希望将原子pi的值设为一个像(a b c)或者可能是(plus 2 2)这样的列表,而不是数字4。为了达到后者,我们再次使用setq操作。举例说明,下面是与Lisp精灵的一段简短对话:

> (setq pie (plus 2 2))
4
> (setq pi '(plus 2 2))
(plus 2 2)

请注意,由于这两个对Lisp精灵提出的要求中仅有的区别是在内部列表(plus 2 2)前面是否存在一个小但关键的引号,而导致分配给原子pie和pi的值之间存在着巨大的差异。在第一个要求中,没有引号,内部的(plus 2 2)必须被求值。这会返回4,该值被赋予变量pie作为其新值。另一方面,在第二个要求中,由于存在引号,列表(plus 2 2)不会被执行为命令,而只是作为一个惰性的Lisp片段被对待,有点像肉铺架子上的肉。它离“活”很近,但却是“死”的。所以在第二种情况下,pi的值是列表(plus 2 2),一段Lisp代码。与精灵的以下交流确认了这些原子的值。

> pie
4
> pi
(plus 2 2)
> (eval pi)
4
>

最后一步是什么意思?我想展示如何让精灵求值一个表达式的值,而不仅仅是打印该表达式的值。通常,精灵自动执行只有一层的求值,但通过写eval,你可以获得第二阶段的求值。(当然,通过反复使用eval,你可以随心所欲地继续这一过程。)这一特性常常被证明是非常宝贵的,但它有点过于先进,在这个阶段不再进一步讨论。

除了nil以外,每个列表至少有一个元素。这个第一个元素被称为列表的Car。因此(eval pi)的car是原子eval。列表(plus 2 2)(setq x 17)(eval pi)(car pi)的car都是操作的名字,或者在Lisp中更常见的称呼是函数。列表的car不必是函数的名字;它甚至不必是原子。例如,((1)(2 2) (3 3 3))就是一个完全合理的列表,它的car是列表(1),而这个car不是一个函数名,只是一个数字。

如果你去掉一个列表的car,剩下的是什么?一个更短的列表。这被称为列表的cdr,这个词听起来介于“kidder”和“could'er”之间。(“car”和“cdr”这两个词是Lisp首次在IBM 704上实现时留下的圣遗物。“car”中的字母代表“寄存器地址部分的内容”,而“cdr”中的字母则代表“寄存器减量部分的内容”,指的是那台机器的具体硬件特性,现在已经无关紧要。)列表(a b c d)的cdr是列表(b c d)(b c d)的cdr是(c d)(c d)的cdr是(d)(d)的cdr是nil。nil既没有cdr也没有car。试图获取nil的car或cdr会导致(或应该导致)Lisp精灵吐出一个错误消息,就像试图除以零应该触发一个错误消息一样。

下面是一个小表格,展示了几个列表的car和cdr,只是为了确保这些概念是明确的。

list car cdr
((a) b (c)) (a) (b (c))
(plus 2 2) plus (2 2)
((car x) (car y)) (car x) ((car y))
(nil nil nil nil) nil (nil nil nil)
(nil) nil nil
nil ERROR ERROR

就像car和cdr被称为函数一样,它们操作的东西被称为参数。因此在命令(plus pie 2)中,plus是函数名,参数是原子pie和2。在评估这个命令(以及大多数命令)时,精灵会找出参数的值,然后将函数应用于这些值。因此,由于原子pie的值是4,原子2的值是2,精灵返回原子6。


假设你有一个列表,并且你想要看到一个与此列表相同,但只有一个元素更长的新列表。例如,假设原子x的值是(cake cookie),你想要创建一个名为y的新列表,与x相同,只是在前面额外加上一个原子——比如说pie。然后你可以使用名为cons(construct的缩写)的函数,其作用是利用一个旧列表和建议的car构造一个新列表。以下是一个这样的过程的记录:

>(setq x '(cake cookie))
(cake cookie)
>(setq y (cons 'pie x))
(pie cake cookie)
> x
(cake cookie)

这里有两点值得注意。我在cons操作后请求打印x的值,这样你可以看到x本身并没有因为cons操作而改变。cons操作创建了一个新列表,并让这个新列表成为y的值,但x完全没有受到影响。另一个值得注意的事实是我再次使用了引号,在原子pie前面。如果没有使用引号会发生什么呢?以下是会发生的情况。

> (setq z (cons pie x))
(4 cake cookie)

记住,毕竟原子pie的值仍然是4,每当精灵在一个愿望中看到未加引号的原子时,它总是使用属于那个原子的值,而不是原子的名字。(总是吗?嗯,几乎总是。我会稍后解释。同时,找找例外——你已经遇到过了。)

现在这里有一些练习题给你——有些可能有点棘手——注意引号!哦,还有一点:我使用了reverse函数,它产生一个与参数相同的列表,只是元素的顺序相反。例如,当精灵被告知(reverse '((a b) (c d e)))时,它会输出((c d e) (a b))。精灵在这次对话中的回答如下。

> (setq w (cons pie '(cdr z)))
> (setq v (cons 'pie (cdr z)))
> (setq u (reverse v))
> (cdr (cdr u))
> (car (cdr u))
> (cons (car (cdr u)) u)
> u
> (reverse '(cons (car u) (reverse (cdr u))))
> (reverse (cons (car u) (reverse (cdr u))))
> u
> (cons 'cookie (cons 'cake (cons 'pie nil)))

精灵的回答(打印结果):

(4 cdr z)
(pie cake cookie)
(cookie cake pie)
(pie)
cake
(cake cookie cake pie)
(cookie cake pie)
((reverse (cdr u)) (car u) cons)
(cake pie cookie)
(cookie cake pie)
(cookie cake pie)

最后一个例子,展示了cons的重复使用,在Lisp俚语中通常被称为“consing up a list”。你从nil开始,然后重复进行cons操作。这类似于从零开始通过不断执行后继操作来构建正整数的过程。然而,虽然在后一过程中,每个阶段都有唯一的方式来执行后继操作,但对于任何一个列表,你可以cons到它上面的不同项有无限多个,从而产生一个庞大的分支列表树,而不是一条不分叉的数字线。正是因为这个从nil的“地面”生长出来并包含所有可能列表的树的形象,我早些时候把nil比作“所有结构扎根的土壤”。

正如我刚才提到的,精灵并不总是用它们的值替换(未加引号的)原子。有些情况下,一个函数即使参数未加引号也会像对待加引号一样处理它们。你有没有回头去找这样一个例子?其实很简单。答案是setq函数。特别是在setq命令中,第一个原子是直接使用的——不进行求值。事实上,setq中的q代表“quote”,意味着第一个参数被当作加引号处理。当学习到set函数时,事情可能会变得相当复杂,它与setq类似,但会对它的第一个参数进行求值。因此,如果原子x的值是原子k,那么说(set x 7)不会对x做任何事——它的值仍然保持为原子k——但是原子k的值现在变成了7。所以请仔细观察:

> (setq a 'b)
> (setq b 'c)
> (setq c 'a)
> (set a c)
> (set c b)

现在告诉我:原子a、b和c的值分别是什么?

答案来了,别偷看。它们分别是:a, a 和 a。这可能看起来有点混乱。你或许可以放心地知道,在Lisp中,set并不是非常常用,这种混淆并不经常出现。从心理层面来说,编程的一个强大之处在于能够基于旧的操作定义新的复合操作,并且可以反复这样做,从而建立起一个越来越复杂的操作库。这让人联想到进化,在进化过程中,更加复杂的分子是从较简单的分子中演变出来的,形成了一个不断上升的复杂性和创造性的螺旋。这也让人想起了工业革命,在工业革命中,人们使用非常简单的早期机器帮助他们建造更复杂的机器,然后再用那些机器建造更加复杂的机器,如此往复,再一次形成了一条不断上升的复杂性和创造性的螺旋。在每一个阶段,不论是进化还是革命,产品都变得更加灵活和精细,更加“智能”,但同时也更容易受到微妙的“bug”或故障的影响。

同样,在Lisp中编程也是如此,只不过这里的“分子”或“机器”现在是用之前已知的Lisp函数定义的Lisp函数。比如,你希望有一个函数,它总是返回列表的最后一个元素,就像car总是返回列表的第一个元素一样。Lisp本身并没有配备这样的函数,但你可以很容易地创建一个。你看懂了吗?要得到名为lyst的列表的最后一个元素,你只需要对lyst进行反转,然后取反转后的列表的car:(car (reverse lyst))。为了将此操作命名为rac(car的反向拼写),我们可以使用def函数,如下所示:

> (def rac (lambda (lyst) (car (reverse lyst))))

以这种方式使用def创建了一个函数定义。在这个定义中,lambda后面跟着(lyst)表明我们正在定义的函数只有一个参数,或者称为虚拟变量,被称作lyst。(它可以被叫做任何名字;我只是恰好喜欢原子lyst。)一般来说,参数列表(虚拟变量)必须紧跟在lambda这个词之后。一旦这个“def愿望”被执行,Lisp精灵对rac函数的理解就和对car函数一样透彻。因此(rac '(your brains))将会产生原子brains。并且我们可以使用rac本身来定义更多的函数。整个过程像滚雪球一样奇迹般地增长,你很快就会被你所掌握的力量所震撼。

这是一个简单的例子。假设你处于一个情境中,你知道你会遇到许多很长的列表,并且知道对于每个这样的长列表,形成一个只包含其car和rac的短列表会很有用。我们可以定义一个单参数函数来为你完成这项工作:

> (def readers-digest-condensed-version
    (lambda (biglonglist)
     (cons (car biglonglist) (cons (rac biglonglist) nil))))

因此,如果我们应用我们新的函数readers-digest-condensed-version到詹姆斯·乔伊斯的《芬尼根的守灵夜》全文(将其视为一个由单词组成的长长的列表),我们将得到一个较短的列表(riverrun the)。不幸的是,再次应用压缩操作符到这个新列表上不会进一步简化它。

如果我们可以创建一个与readers-digest-condensed-version相对的操作,称之为rejoyce,给定任意两个词,它能创造出一部以这两个词开头和结尾的小说——而且这部小说应该是詹姆斯·乔伊斯写的(如果他想到了的话)。那么这将是既美好又实用的。因此,执行Lisp语句(rejoyce 'Stately 'Yes)将会使Lisp精灵从头开始生成整部小说《尤利西斯》。编写这个函数留给读者作为练习。为了测试你的程序,看看它对(rejoyce 'karma 'dharma)会做出什么反应。

有些人认为使用Lisp及相关编程语言可以实现一个既理想又可行的目标是:(1)让每一条语句都返回一个值;(2)通过这个返回值,并且只有通过这个返回值,该语句才能产生任何效果。想法(1)是值从内层函数调用传递到外层函数调用,直到完整的语句值返回给你。想法(2)是在所有这些调用过程中,没有任何原子的值被改变(除非该原子是虚拟变量)。在我所知的所有Lisp方言中,(1)是真的,但(2)并不一定成立。

因此,如果x绑定到(a b c d e),然后你说(car (cdr (reverse x))),首先发生的是计算(reverse x);然后这个值被“向上”传递给cdr函数,该函数计算那个列表的cdr;最后,这个较短的列表被传递给car函数,它提取一个元素——即原子d——并返回它。与此同时,原子x并未受损;它仍然绑定到(a b c d e)

可能看起来像是(reverse x)这样的表达式会通过反转x来改变x的值,就像口头命令“把毛衣翻过来”会影响毛衣一样。但实际上,执行愿望(reverse x)并不会比执行愿望(plus 2 2)改变2的值更多。相反,执行(reverse x)会导致一个新的(无名)列表诞生,就像x一样,只不过是反转过的。这个列表就是语句的值;这就是语句返回的内容。而x本身的值则保持不变。同样,评估(cons 5 pi)也不会对名为pi的列表产生丝毫影响;它只是返回一个新的列表,其中5作为car,而pi的值作为cdr。

这种行为与留下“副作用”的函数形成对比。这样的副作用通常表现为变量绑定的变化,尽管也有其他可能性,比如引起输入或输出的发生。一个典型的“有害”命令是setq,而支持“适用性”("applicative")编程学派的人——这个学派主张你不应该制造任何副作用——对仅仅提到setq都会感到深深的不安。对他们来说,所有的结果必须纯粹通过函数计算它们的值并将这些值传递给其他函数的方式产生。

适用性风格的支持者所认可的唯一绑定是短暂的“lambda绑定”——即当函数应用于其参数时产生的那些绑定。每当调用任何函数时,该函数的虚拟变量暂时假定“lambda绑定”。这些绑定就像由setq引起的绑定一样,只是它们是短暂的。也就是说,一旦函数完成计算,它们就会消失——不留痕迹。例如,在计算(rac '(a b c))的过程中,虚拟变量lyst的lambda绑定是列表(a b c);但是一旦答案c被传递给请求rac的函数或人,用于得到那个答案的原子lyst的值就被完全忘记了。如果你询问lyst的值,Lisp解释器会告诉你lyst是一个“未绑定的原子”。适用性程序员更倾向于lambda绑定而非普通的setq绑定。

我个人并不是一个避免使用setq和其他导致副作用函数的狂热者。虽然我发现适用性风格确实优雅,但在构建大型AI风格程序时,我觉得它不切实际。因此,我在这里不会提倡适用性风格,尽管在可能的情况下我会遵循它。严格来说,在适用性编程中,你甚至不能定义新的函数,因为def语句会在Lisp精灵的记忆中造成永久性的变化——即永久地将函数定义存储在内存中。所以理想的适用性方法会有函数,就像变量绑定一样,只临时创建,并且一旦使用过它们的定义就会被丢弃。但这是一种极端的“适用主义”。

为了让你了解更多信息,这里还有一些简单的函数定义。

> (def rdc (lambda (lyst) (reverse (cdr (reverse lyst)))))
> (def snoc (lambda (x lyst) (reverse (cons x (reverse lyst)))))
> (def twice (lambda (n) (plus n n)))

rdc和snoc函数与cdr和cons相似,只是方向相反。因此,(a b c d e)的rdc是(a b c d),如果你输入(snoc 5 '(1 2 3 4)),你将得到(1 2 3 4 5)作为答案。

到目前为止,这一切都还算有趣,但如果你想看到Lisp精灵做一些真正令人惊讶的事情,你就必须允许它根据途中发生的事情做出一些决定。这些有时被称为“条件愿望”。一个典型的例子如下:

> (cond ((eq x 1) 'land) ((eq x 2) 'sea))

如果x的值为1,则该语句返回的值将是原子land;如果x的值为2,则返回的值将是原子sea。否则,返回的值将是nil(即如果x是5)。原子eq(发音为“eek”)是Lisp中常用的一个函数名,如果它的两个参数具有相同的值,它将返回原子t(代表“真”),如果它们的值不同,则返回nil(代表“否”或“假”)。

cond语句是一个列表,其car是函数名cond,后面跟着任意数量的cond子句,每个子句都是一个两元素的列表。每个子句的第一个元素称为条件,第二个元素称为结果。Lisp精灵会按顺序逐一检查子句的条件;一旦发现一个条件为“真”(意味着条件返回的不是nil!),它就开始计算那个子句的结果,其值将成为整个cond语句的返回值。之后的子句甚至都不会被浏览!这听起来可能比实际情况复杂。实际上的想法很简单,就是寻找第一个满足条件的情况,然后返回相应的结果。

通常,人们希望在末尾有一个兜底子句,其条件必定会被满足,这样即使所有其他条件都不成立,至少这一个条件是真实的,并且回传的结果会是某个值而不是nil。创建一个非nil值的条件非常简单;比如,可以选择它为t,如下所示:

(cond ((eq x 1) 'land)
      ((eq x 2) 'sea)
       (t 'air))

根据x的值,这个cond语句将返回land、sea或air中的一个,但我们永远不会得到nil。下面是几个供你尝试的cond语句示例:

> (cond ((eq (oval pi) pie) (oval (snot pie pi)))
(t (eval (snoc (rac pi) pi))))
> (cond ((eq 2 2) 2) ((eq 3 3) 3))
> (cond (nil 'no-no-no)
((eq '(car nil) '(cdr nil)) 'hmmm)
(t 'yes-yes-yes))

答案分别是:8, 2, 和 yes-yes-yes。你注意到(car nil)(cdr nil)被引用了吗?

我将以展示一组模式化的函数定义来结束这一部分,它们的模式如此明显,以至于你会认为Lisp精灵在看了前几个后就能掌握规律...不幸的是,Lisp精灵出奇地迟钝(或者至少它们表现得很迟钝),除非结论被完全明确表述出来,否则它们不会得出任何结论。先来看看这些函数家族:

> (def square (lambda (k) (times k k)))
> (def cube (lambda (k) (times k (square k))))
> (def 4th-power (lambda (k) (times k (cube k))))
> (def 5th-power (lambda (k) (times k (4th-power k))))
> (def 6th-power (lambda (k) (times k (5th-power k))))
> .
> .
> .
> .

我要问你的问题是:你能发明一个定义,用一个双参数函数一次性概括所有这些吗?更具体地说,问题是如何定义一个名为power的双参数函数,使得,例如,(power 9 3)在求值时产生729,而(power 7 4)产生2,401?我在本文中已经提供了所有必要的工具,只要你发挥一些创造力就能做到这一点。

我想以一则关于新发现生物的消息来结束这一专栏——丑陋的格拉祖基亚豪猪鱼,之所以这样称呼是因为它只在格拉祖基亚岛上被发现(虽然该岛位于伯尔诺梅德海岸附近,但被上比特波宣称拥有主权)。豪猪鱼是什么呢?这是一种奇特的豪猪品种,其刺——出于某种原因,在外格拉祖基亚总是恰好有九根,在内陆格拉祖基亚则是七根——是更小的豪猪鱼。哦哦!这似乎会造成无限递归!但并非如此。我只是忘记提到了有一种最小尺寸的豪猪鱼:零英寸型,这种豪猪鱼令人惊讶地完全没有刺。因此,很幸运地(或者不幸地,取决于你的观点),这阻止了潜在的无限递归。这种非凡的生物在图17-1中有一张罕见的照片。

动物学的学生可能会感兴趣地了解到,5英寸豪猪鱼上的刺总是4英寸豪猪鱼,以此类推。人类学的学生可能同样会对这样一个事实感到好奇:格拉祖基亚(无论是外还是内)的居民使用零英寸豪猪鱼的鼻子作为交易单位——对于我们来说这是一件奇怪的事;但话说回来,你我有何资格质疑外格拉祖基亚和内陆格拉祖基亚的古老智慧呢?因此,由于一个较大的豪猪鱼——比如说3英寸或4英寸的——含有许多这样的小鼻子,它就成了非常有价值的商品。豪猪鱼的价值有时被称为其“购买力”,或简称“力量”。例如,在内陆格拉祖基亚找到的一只2英寸豪猪鱼的价值几乎是外格拉祖基亚相同大小豪猪鱼的两倍。或者是我记反了?这真是令人困惑!

不管怎样,我为什么要告诉你这些?哦,我只是认为你会想听听这件事。此外,谁知道呢?也许有一天你会访问格拉祖基亚(无论是内陆还是外)。那时,这些知识可能会非常有用。