计算机程序的构造和解释(JavaScript版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

序言

在学生时代,遇到妙趣横生的Alan Perlis时我总是非常兴奋,并和他有几次交谈。他和我对两种截然不同的程序设计语言——Lisp和APL——都非常喜爱和珍重。跟上他的步伐很不容易,何况他还在开辟卓越的新路。我想重新检视他在给这本书原序中的一个论断(我建议你在读完本序言后再重读他的序,它就印在后面):用100个函数在一种数据结构上操作,优于用10个函数在10种数据结构上操作。这句话真对吗?

为了认真回答这个问题,我们首先要问,这一种数据结构是“普适的”吗?它能方便地取代那10种更特殊的数据结构的角色吗?

对于这些,我们还可以问,我们真的需要这100个函数吗?是否存在一个单一的“普适”函数,它能取代所有其他函数的角色?

对最后这个问题,有一个令人惊异的回答:是!只需要不多的技巧就能构造出一个函数,该函数接受(1)一种数据结构,它可以看作其他函数的描述,以及(2)一系列的参数,使该函数的行为恰如前面的那些函数作用于这些给定的参数。同时,只需要不多的技巧就能设计出一种数据结构,使其足以描述我们需要的所有计算。一种这样的数据结构(用于表示表达式和语句的带标签表,加上记录名字与值的关联的环境)和一个这样的普适函数(apply)将在本书的第4章介绍。也就是说,我们可能只需要一个函数和一种数据结构。

这些在理论上都是对的,但在实践中,我们发现区分不同事物确实很有帮助。作为人,在需要构造计算的描述时,应该把我们代码的结构组织好,使我们更好地理解它们。我相信Perlis在这里不是想讨论计算能力的问题,而是要讨论人的能力及其限度。

人的头脑似乎在为事物命名方面做得很好。我们有强大的关联记忆,给我们一个名字,我们可以很快想起与之关联的某些事物。这可能就是我们会发现使用lambda演算很方便,而使用组合演算就不太容易的原因。对大多数人而言,给他们解释Lisp表达式(lambda(x)(lambda(y)(+x y)))或JavaScript表达式x=> y=> x+y都比较容易,而解释下面的组合表达式就难得多了:

((S((S(K S))((S((S(K S))((S(K K))(K+))))((S(K K))I))))(K I))

即使将其写成结构与之对应的5行Lisp代码,事情也不会变得更容易。

所以,虽然从原则上说,我们可以只用一个普适函数,但却更应该把代码模块化,并为其中的各种片段命名。然后在这个普适函数里用名字来说这些函数的描述,而不是简单地把这些函数的描述直接塞进代码里。

我在1998年的演讲“生长出一种语言”里曾经说过,一个好程序员“并不只是写程序,好程序员要构造有用的词汇表”。随着设计和定义出程序中越来越多的部分,我们要给这些部分命名,这样做的结果就是生长出了一个日益丰富的、能用于描述其他部分的语言。

然而我们也发现,与区分不同的数据结构相比,为它们命名更不容易。

嵌套的表可以看作一种普适数据结构(值得说一下,很多时髦的、使用广泛的数据结构,例如HTML、XML和JSON,也都是各种括号括起的嵌套表示形式,只不过比Lisp的简单括号形式更精致一点)。还有许多函数在范围广泛的许多不同情景下都很有用,例如确定一个表的长度,把一个函数应用于一个表的每个元素并返回结果的表等。因此,当我思考某项特殊的计算时,就经常对自己说,“这个以两个东西为元素的表,我期望它表示一个人的姓和名;那个以两个东西为元素的表,我期望它表示一个复数的实部和虚部;另一个以两个东西为元素的表,我期望它表示一个分数的分子和分母”,如此等等。换句话说,我对它们做了区分——因此,明确地表示这些数据结构之间的区分,也可能非常有用。一种作用就是可以防止一些错误,例如无意中错误地把复数当作分数使用。(再次强调,这一注释也是有关人的能力及其限度。)

在写本书的第1版时,那是在大约40年前,许多数据组织方式已经成为相对的标准,特别是“面向对象”技术。还有很多程序设计语言(包括JavaScript)支持一些特殊数据结构,例如对象和字符串、堆和映射,还有许多内置的或者库支持的数据机制。但是,在这样做的同时,许多语言放弃了对更一般、更普适的描述机制的支持。以Java为例,开始时它不支持函数作为一等元素,新近才把这种功能结合进来,这极大地提高了表达能力。

类似地,APL原来也不支持函数作为一等元素,而且它原本只支持一种数据结构——各种维数的数组。以数组作为普适数据结构非常不方便,因为数组不能以其他数组作为元素。APL的新近版本也支持了匿名的函数值和嵌套的数组,这些扩充奇迹般地增强了APL的表达能力。(APL的原初设计确实包含了两个非常好的特点:它有一集容易理解的函数,它们都应用于唯一的一种数据结构,这些函数还被特别选择了一套名字。我这里不是说那些奇怪的符号或希腊字母,而是APL程序员在使用函数时所用的词汇,例如shape、reshape、compress、expand和laminate等。这些都是名字而不是符号,它们说明了函数的功能。Ken Iversion在为操作数组的函数取短小易记而且生动的名字方面确有些高招。)

至于JavaScript,与Java类似,最初设计时心里想的就是对象和方法,但一开始就纳入了一等函数,用它的对象定义普适数据结构也没有任何困难。由于这些情况,JavaScript与Lisp的距离不像你想象的那么远。因此,正如这一版《计算机程序的构造和解释》展示的,它可以作为表达相关核心思想的另一个框架。SICP并不是讨论某种程序设计语言的,它展示的是有关程序组织的一些强大且具有普适性的思想,因此应该适用于任何程序设计语言。

Lisp和JavaScript有哪些共性?把一项计算(代码加一些相关数据结构)抽象为一个可用于在将来执行的函数的能力;针对一些参数调用函数的能力;划定一些不同情况(条件执行)的能力;一种方便的普适数据结构;对数据的完全自动化的存储管理(这种功能初看起来无足轻重,好像什么都没说,直到你认识到许多广泛使用的程序设计语言都缺少了这种功能);很大一集操控普适数据结构的非常有用的函数;以及使用普适数据结构去表示各种更特殊的数据结构的一套标准策略。

因此,真理很可能位于Perlis雄辩地设置的两个极端之间。甜蜜点可能更像是某种情况,例如针对一种普适数据结构(例如表)完成各种操作的40个足够普适的有用函数,再加上10组每组各6个函数,分别用于操控该普适数据结构的10种特殊视角。

当你阅读这本书时,请不要只关心各种程序设计语言的结构及其使用,还应该关注赋予各个函数、变量和数据结构的名字。这些名字并不都简短并生动,如Iverson为其APL语言选的名字。但它们也经过精心地、系统化地选择,可以加深你对整个程序结构的理解。

基本操作、组合方法、功能抽象、命名,以及为了做出各种区分而使用的普适数据结构的各种常用定制方法,这些都是一个好的程序设计语言的基本构造要素。从这些出发,再加上想象和基于经验的良好的工程评判能力,就能做好其他事情。

Guy L.Steele Jr.

马萨诸塞州列克星敦,2021