书呆子的复仇
Original2002 年 5 月
“我们追逐的是 C++ 程序员。我们成功地将他们中的许多人拉到了 Lisp 的一半。”
- Java 规范的共同作者 Guy Steele
在软件行业,尖头学者与另一股同样强大的力量——尖头老板之间一直存在着斗争。每个人都知道尖头老板是谁,对吧?我想科技界的大多数人不仅认识这个卡通人物,而且知道他们公司里以他为原型的真正人物。
这位尖头发的老板奇迹般地结合了两种本身很常见但很少同时出现的品质:(a)他对技术一无所知,(b)他对技术有非常强烈的看法。
比如说,假设你需要编写一个软件。头发尖尖的老板不知道这个软件该怎么运行,也分不清编程语言,但他知道你应该用什么语言来写。没错。他认为你应该用 Java 来写。
他为什么会这么想呢?让我们来看看这位头发尖尖的老板的大脑。他的想法是这样的。Java 是一个标准。我知道它一定是,因为我经常在媒体上读到它。既然它是一个标准,我不会因为使用它而惹上麻烦。这也意味着会一直有大量的 Java 程序员,所以如果现在为我工作的程序员辞职,就像为我工作的程序员总是神秘地辞职一样,我可以轻松地替换他们。
好吧,这听起来并不是那么不合理。但这一切都基于一个不言而喻的假设,而这个假设被证明是错误的。这位头发尖尖的老板认为所有编程语言都差不多。如果这是真的,那他就说对了。如果所有语言都差不多,当然,使用其他人使用的语言。
但并非所有语言都是等价的,我想我可以向你证明这一点,甚至不需要讨论它们之间的差异。如果你在 1992 年问这位头发尖尖的老板应该用什么语言编写软件,他会毫不犹豫地回答,就像他今天一样。软件应该用 C++ 编写。但如果所有语言都是等价的,那为什么这位头发尖尖的老板的观点会改变呢?事实上,为什么 Java 的开发人员甚至要费心去创建一种新语言呢?
假设你创建了一种新语言,那是因为你认为它在某些方面比人们已有的语言更好。事实上,Gosling 在第一份 Java 白皮书中明确指出,Java 的设计是为了解决 C++ 的一些问题。所以你明白了:并非所有语言都是等价的。如果你顺着这位头发尖尖的老板的思路追溯到 Java,然后再追溯到 Java 的历史和起源,你最终会得出一个与你最初的假设相矛盾的想法。
那么,谁是对的?詹姆斯·高斯林,还是那个头发尖尖的老板?毫不奇怪,高斯林是对的。有些语言在某些问题上比其他语言更好。你知道,这引发了一些有趣的问题。Java 的设计目的就是在某些问题上比 C++ 更好。什么问题?什么时候 Java 更好,什么时候 C++ 更好?是否存在其他语言比它们任何一种都更好的情况?
一旦你开始考虑这个问题,你就打开了一个真正的潘多拉魔盒。如果这位头发尖尖的老板必须考虑这个问题的复杂性,那他的大脑就会爆炸。只要他认为所有语言都是等价的,他所要做的就是选择看起来最有动力的语言,而且因为这更多的是一个时尚问题而不是技术问题,甚至他也可能得到正确的答案。但如果语言各不相同,他突然必须解决两个同时发生的方程,试图在他一无所知的两件事之间找到最佳平衡:二十种左右的领先语言对他需要解决的问题的相对适用性,以及为每种语言找到程序员、库等的可能性。如果这就是门的另一边,那么这位头发尖尖的老板不想打开它也就不足为奇了。
认为所有编程语言都等同的缺点在于,事实并非如此。但优点在于,它让你的生活变得简单很多。我认为这是这个想法如此广泛传播的主要原因。这是一个令人舒服的想法。
我们知道 Java 一定很棒,因为它是一种很酷的新编程语言。真的是这样吗?如果你从远处看编程语言的世界,Java 似乎是最新的语言。(从足够远的地方,你只能看到 Sun 公司出资制作的大型闪烁广告牌。)但如果你近距离观察这个世界,你会发现酷是有等级之分的。在黑客亚文化中,还有一种叫做 Perl 的语言被认为比 Java 酷得多。例如,Slashdot 就是由 Perl 生成的。我认为你不会发现这些人使用 Java Server Pages。但还有另一种较新的语言,叫做 Python,它的用户往往看不起 Perl,而更多的人则在虎视眈眈。
如果你按 Java、Perl、Python 的顺序查看这些语言,你会发现一个有趣的模式。至少,如果你是一名 Lisp 黑客,你会注意到这个模式。每一种语言都越来越像 Lisp。Python 甚至复制了许多 Lisp 黑客认为是错误的功能。你可以逐行将简单的 Lisp 程序翻译成 Python。现在是 2002 年,编程语言几乎赶上了 1958 年的水平。
追赶数学
我的意思是,Lisp 最早是由约翰·麦卡锡于 1958 年发现的,而流行的编程语言直到现在才赶上他当时发展的思想。
那么,这怎么可能呢?计算机技术不是变化非常快吗?我的意思是,在 1958 年,计算机是冰箱大小的庞然大物,处理能力堪比手表。那么古老的技术怎么可能与最新发展相符,更不用说超越了?
我来告诉你怎么做。这是因为 Lisp 并不是真正设计成一种编程语言,至少不是我们今天所指的那种编程语言。我们所说的编程语言是我们用来告诉计算机做什么的东西。麦卡锡最终确实打算开发一种这种意义上的编程语言,但我们最终得到的 Lisp 是基于他作为理论练习所做的另一件事——努力定义一种比图灵机更方便的替代方案。正如麦卡锡后来所说,
另一种证明 Lisp 比图灵机更简洁的方法是编写一个通用的 Lisp 函数,并证明它比通用图灵机的描述更简洁、更易懂。这就是 Lisp 函数eval ...,它计算 Lisp 表达式的值……编写eval需要发明一种表示 Lisp 函数作为 Lisp 数据的符号,这种符号是为了论文的目的而设计的,并没有想到它会在实践中用来表达 Lisp 程序。
接下来发生的事情是,1958 年末的某个时候,麦卡锡的一名研究生史蒂夫·拉塞尔 (Steve Russell) 研究了eval的定义并意识到,如果他将其翻译成机器语言,结果将是一个 Lisp 解释器。
这在当时是一个巨大的惊喜。以下是麦卡锡后来在一次采访中对此的评论:
史蒂夫·拉塞尔说,你看,我为什么不编写这个eval程序……,我对他说,嘿,嘿,你把理论和实践混为一谈了,这个eval是用来阅读的,不是用来计算的。但他还是继续做了。也就是说,他把我论文中的eval编译成 [IBM] 704 机器代码,修复了错误,然后将其宣传为 Lisp 解释器,它确实是。所以那时 Lisp 基本上就形成了今天的形式……
突然间,我想大概在几周之内,麦卡锡发现他的理论练习就变成了一种真正的编程语言——而且比他预想的更强大。
所以,为什么这种 20 世纪 50 年代的语言没有过时呢?简而言之,它不是技术,而是数学,而数学不会过时。Lisp 的正确比较对象不是 20 世纪 50 年代的硬件,而是 Quicksort 算法,该算法于 1960 年被发现,至今仍是最快的通用排序算法。
20 世纪 50 年代还存在着另外一种语言,那就是 Fortran,它代表了与语言设计截然相反的方法。Lisp 是一种理论,却意外地变成了一种编程语言。Fortran 是专门为一种编程语言而开发的,但我们现在认为它是一种非常低级的语言。
Fortran I是 1956 年开发的语言,与当今的 Fortran 截然不同。Fortran I 基本上是带有数学的汇编语言。在某些方面,它不如最近的汇编语言强大;例如,没有子程序,只有分支。现在的 Fortran 可以说更接近 Lisp 而不是 Fortran I。
Lisp 和 Fortran 是两棵独立进化树的主干,一棵植根于数学,一棵植根于机器架构。这两棵树从那时起就一直在融合。Lisp 一开始就很强大,在接下来的二十年里发展得很快。所谓的主流语言一开始发展得很快,在接下来的四十年里逐渐变得更强大,直到现在,其中最先进的语言已经相当接近 Lisp。很接近,但它们仍然缺少一些东西……
Lisp 的与众不同之处
Lisp 最初开发时就体现了九个新理念。其中一些理念如今已习以为常,另一些理念只在更高级的语言中出现,还有两个理念至今仍是 Lisp 独有的。这九个理念按被主流采用的顺序排列如下:
条件。条件是一种 if-then-else 结构。我们现在认为这些是理所当然的,但 Fortran I 没有它们。它只有一个紧密基于底层机器指令的条件 goto。
函数类型。在 Lisp 中,函数是一种数据类型,就像整数或字符串一样。它们具有文字表示,可以存储在变量中,可以作为参数传递,等等。
递归。Lisp 是第一个支持递归的编程语言。
动态类型。在 Lisp 中,所有变量实际上都是指针。值才是具有类型的,而不是变量,赋值或绑定变量意味着复制指针,而不是它们指向的内容。
垃圾收集。
由表达式组成的程序。Lisp 程序是表达式树,每个表达式都返回一个值。这与 Fortran 和大多数后续语言不同,后者区分了表达式和语句。
Fortran I 中存在这种区别是很自然的,因为您无法嵌套语句。因此,虽然您需要表达式才能进行数学运算,但让其他任何事物返回值都是没有意义的,因为不可能有任何东西在等待它。
随着块结构语言的出现,这一限制消失了,但那时已经太迟了。表达式和语句之间的区别已经根深蒂固。它从 Fortran 传播到 Algol,然后传播到它们的后代。
符号类型。符号实际上是指向存储在哈希表中的字符串的指针。因此,您可以通过比较指针来测试相等性,而不是比较每个字符。
使用符号和常量树的代码表示法。
整个语言始终存在。读取时、编译时和运行时之间没有真正的区别。您可以在读取时编译或运行代码,在编译时读取或运行代码,也可以在运行时读取或编译代码。
在读取时运行代码让用户可以重新编程 Lisp 的语法;在编译时运行代码是宏的基础;在运行时编译是 Lisp 在 Emacs 等程序中用作扩展语言的基础;而在运行时读取使程序能够使用 s 表达式进行通信,这个想法最近被重新定义为 XML。
当 Lisp 首次出现时,这些思想与普通的编程实践相去甚远,而普通的编程实践很大程度上是由 20 世纪 50 年代末的硬件决定的。随着时间的推移,默认语言(体现在一系列流行语言中)逐渐向 Lisp 演进。思想 1-5 现在已广为流传。思想 6 开始出现在主流中。Python 有 7 的形式,尽管它似乎没有任何语法。
至于第 8 点,这可能是最有趣的。第 8 点和第 9 点想法只是偶然成为 Lisp 的一部分,因为 Steve Russell 实现了 McCarthy 从未打算实现的东西。然而,这些想法最终导致了 Lisp 的奇怪外观和最显著的特征。Lisp 看起来很奇怪,不是因为它有奇怪的语法,而是因为它没有语法;你直接在解析其他语言时在后台构建的解析树中表达程序,这些树由列表组成,列表是 Lisp 的数据结构。
用语言自己的数据结构来表达语言是一项非常强大的功能。第 8 条和第 9 条结合起来意味着您可以编写编写程序的程序。这听起来可能是一个奇怪的想法,但它是 Lisp 中的日常事物。最常见的方法是使用一种称为宏的东西。
“宏”一词在 Lisp 中的含义与其他语言中的含义不同。Lisp 宏可以是任何东西,从缩写到新语言的编译器。如果你想真正理解 Lisp,或者只是拓展你的编程视野,我建议你多了解一下宏。
据我所知,宏(在 Lisp 意义上)仍然是 Lisp 独有的。部分原因是,为了拥有宏,你可能必须让你的语言看起来和 Lisp 一样奇怪。也可能是因为,如果你真的增加了最后一步,你就不能再声称发明了一种新语言,而只能说是一种新的 Lisp 方言。
我提到这一点主要是开玩笑,但这是真的。如果你定义了一种语言,它有 car、cdr、cons、quote、cond、atom、eq 以及表示为列表的函数符号,那么你就可以用它构建 Lisp 的其余部分。事实上,这就是 Lisp 的决定性品质:正是为了实现这一点,麦卡锡才赋予了 Lisp 现在的形状。
语言至关重要
那么,假设 Lisp 确实代表了主流语言正在逐渐接近的一种极限——这是否意味着你应该用它来编写软件呢?使用功能较弱的语言会给你带来多大的损失?有时候,不站在创新的最前沿不是更明智吗?而且,在某种程度上,受欢迎程度本身不就是正当的吗?例如,尖头发的老板想要使用一种他可以轻松雇佣程序员的语言,难道不是对的吗?
当然,有些项目对编程语言的选择并不重要。一般来说,应用程序的要求越高,使用强大的语言的优势就越大。但很多项目的要求并不高。大多数编程可能都是编写一些小的粘合程序,对于小的粘合程序,您可以使用任何您熟悉的语言,只要它有很好的库,可以满足您的任何需求。如果您只需要将数据从一个 Windows 应用程序传输到另一个,当然可以使用 Visual Basic。
你也可以用 Lisp 编写一些小的粘合程序(我把它用作桌面计算器),但像 Lisp 这样的语言的最大优势在于另一个极端,你需要编写复杂的程序来解决激烈竞争中的难题。一个很好的例子就是 ITA Software 授权给 Orbitz 的机票搜索程序。这些家伙进入了一个已经被两大竞争对手 Travelocity 和 Expedia 主导的市场,似乎刚刚在技术上羞辱了它们。
ITA 应用程序的核心是一个 20 万行的 Common Lisp 程序,它搜索的可能性比竞争对手高出许多个数量级,而他们的竞争对手显然仍在使用大型机时代的编程技术。(尽管 ITA 在某种意义上也在使用大型机时代的编程语言。)我从未见过 ITA 的任何代码,但据他们的一位顶级黑客说,他们使用了很多宏,听到这个我并不感到惊讶。
向心力
我并不是说使用不常见的技术没有成本。这位头发尖尖的老板对此的担心并非完全错误。但由于他不了解风险,他往往会夸大风险。
我能想到使用不太常见的语言可能带来的三个问题。你的程序可能无法很好地与用其他语言编写的程序兼容。你可以使用的库可能更少。而且你可能很难招到程序员。
这些问题各有多大?第一个问题的重要性取决于你是否能控制整个系统。如果你编写的软件必须在远程用户的机器上运行,而这个机器又是一个漏洞百出的封闭操作系统(我没点出具体的名字),那么用与操作系统相同的语言编写应用程序可能会有优势。但是如果你能控制整个系统,并且拥有所有部分的源代码,而 ITA 大概就是这样,那么你可以使用任何你想要的语言。如果出现任何不兼容的情况,你可以自己修复。
在基于服务器的应用程序中,你可以使用最先进的技术,我认为这是乔纳森·埃里克森所说的“编程语言复兴”的主要原因。这就是为什么我们甚至听说了 Perl 和 Python 等新语言。我们听说这些语言不是因为人们用它们编写 Windows 应用程序,而是因为人们在服务器上使用它们。随着软件从桌面转移到服务器(微软似乎也承认这一未来),使用中庸技术的压力将越来越小。
至于库,其重要性还取决于应用程序。对于要求不高的问题,库的可用性可能比语言的内在功能更重要。盈亏平衡点在哪里?很难说清楚,但无论在哪里,它都缺少任何你可能称之为应用程序的东西。如果一家公司认为自己从事软件业务,并且他们正在编写一个将成为其产品之一的应用程序,那么它可能会涉及几名黑客,并且至少需要六个月的时间来编写。在这种规模的项目中,强大的语言可能开始胜过预先存在的库的便利性。
尖头发老板的第三个担忧是难以招聘程序员,我认为这只是个幌子。毕竟,你需要招聘多少黑客?现在我们都知道,软件最好由少于 10 人的团队开发。对于任何你听说过的语言,你都不应该为招聘如此规模的黑客而烦恼。如果你找不到 10 名 Lisp 黑客,那么你的公司可能位于不适合开发软件的城市。
事实上,选择一种更强大的语言可能会减少您所需的团队规模,因为(a)如果您使用一种更强大的语言,您可能不需要那么多黑客,并且(b)使用更高级语言工作的黑客可能更聪明。
我并不是说你不会面临很大的压力去使用所谓的“标准”技术。在 Viaweb(现在的 Yahoo Store),我们使用 Lisp 引起了一些风险投资人和潜在收购者的不满。但我们也因为使用通用的 Intel 机器作为服务器而不是像 Sun 这样的“工业强度”服务器、使用当时鲜为人知的开源 Unix 变体 FreeBSD 而不是像 Windows NT 这样的真正的商业操作系统、忽略了现在甚至没人记得的所谓电子商务标准SET等而引起了不满。
你不能让那些大人物替你做技术决定。我们使用 Lisp 是否让一些潜在的收购者感到震惊?有些人确实感到震惊,但如果我们没有使用 Lisp,我们就无法编写出让他们想要收购我们的软件。在他们看来,这似乎是一种异常,但实际上却有因果关系。
如果你创办一家初创公司,不要为了取悦风险投资人或潜在收购者而设计你的产品。*设计你的产品是为了取悦用户。*如果你赢得了用户,其他一切都会水到渠成。如果你没有,没有人会在意你的技术选择有多么令人欣慰的正统。
平庸的代价
使用功能较弱的语言会造成多大的损失?实际上有一些数据可以说明这一点。
最方便的衡量能力的指标可能是代码大小。高级语言的目的是为您提供更大的抽象——就像更大的砖块一样,这样您就不需要那么多砖块来建造给定大小的墙。因此,语言越强大,程序就越短(当然,不仅仅是字符,而是不同元素)。
更强大的语言如何帮助您编写更短的程序?如果语言允许您,您可以使用一种称为自下而上的编程的技术。您不必简单地用基础语言编写应用程序,而是在基础语言之上构建一种用于编写类似程序的语言,然后用它编写程序。组合代码可能比您用基础语言编写整个程序要短得多——事实上,大多数压缩算法就是这样工作的。自下而上的程序也应该更容易修改,因为在许多情况下语言层根本不需要改变。
代码大小很重要,因为编写程序所需的时间主要取决于程序的长度。如果你的程序用另一种语言编写时是原来的三倍长,那么编写它所需的时间也会是原来的三倍——而且你无法通过雇佣更多人来解决这个问题,因为超过一定规模后,新雇佣的人实际上是净损失。弗雷德·布鲁克斯 (Fred Brooks) 在他的名著*《人月神话》中描述了这种现象,*我所看到的一切都证实了他的说法。
那么,如果你用 Lisp 编写程序,程序会短多少呢?例如,我听到的大多数 Lisp 与 C 相比的数字大约是 7-10 倍。但《新建筑师》杂志最近发表的一篇关于 ITA 的文章说,“一行 Lisp 可以取代 20 行 C”,由于这篇文章中引用了 ITA 总裁的话,我猜他们从 ITA 那里得到了这个数字。如果是这样,那么我们可以相信它;ITA 的软件包含大量 C 和 C++ 以及 Lisp,所以他们是根据经验说的。
我猜这些倍数甚至不是恒定的。我认为当你遇到更困难的问题时,以及当你有更聪明的程序员时,它们会增加。一个真正优秀的黑客可以从更好的工具中榨取更多。
无论如何,作为曲线上的一个数据点,如果你与 ITA 竞争并选择用 C 语言编写软件,那么他们开发软件的速度将比你快二十倍。如果你花了一年时间开发一个新功能,他们能够在不到三周的时间内复制它。而如果他们只花了三个月开发一个新东西,那么你也要五年才能拥有它。
你知道吗?这是最好的情况。当你谈论代码大小比率时,你隐含地假设你实际上可以用较弱的语言编写程序。但事实上,程序员的能力是有限的。如果你试图用一种太低级的语言来解决一个难题,那么你就会到达一个临界点,脑子里一下子想不起来的东西太多了。
因此,当我说 ITA 的假想竞争对手需要五年时间才能复制 ITA 用 Lisp 在三个月内写出的东西时,我的意思是,如果不出意外的话,五年时间。事实上,按照大多数公司的运作方式,任何需要五年时间的开发项目很可能永远都无法完成。
我承认这是个极端的例子。ITA 的黑客似乎异常聪明,而 C 是一种相当低级的语言。但在竞争激烈的市场中,即使是二比一或三比一的差距也足以保证你永远落后。
食谱
这种可能性是尖头发的老板根本不想考虑的。所以大多数人都不会考虑。因为,你知道,归根结底,只要没有人能证明这是他的错,尖头发的老板并不介意他的公司被打得屁滚尿流。对他个人来说,最安全的计划是紧跟群体的中心。
在大型组织中,用来描述这种方法的短语是“行业最佳实践”。其目的是让这位头发尖尖的老板逃避责任:如果他选择了“行业最佳实践”,而公司遭受损失,那也不能怪他。这不是他选择的,而是行业的选择。
我认为这个术语最初是用来描述会计方法等的。它的大致意思是*不要做任何奇怪的事情。*在会计方面,这可能是一个好主意。“尖端”和“会计”这两个词放在一起听起来不太好。但当你把这个标准引入到技术决策中时,你就会开始得到错误的答案。
技术往往应该是尖端的。正如 Erann Gat 所指出的,在编程语言中,“行业最佳实践”实际上带给你的并不是最好的,而只是平均水平。当一个决定导致你开发软件的速度只是更激进的竞争对手的一小部分时,“最佳实践”就是一个误称。
所以这里有两条我认为非常有价值的信息。事实上,我从自己的经验中知道这一点。第一,语言的力量各不相同。第二,大多数经理故意忽略这一点。这两个事实实际上是赚钱的秘诀。ITA 就是这个秘诀的一个例子。如果你想在软件行业中取胜,就解决你能找到的最难的问题,使用你能找到的最强大的语言,等待你的竞争对手的老板们恢复平庸。
附录:电源
为了说明我所说的编程语言的相对强大之处,请考虑以下问题。我们想编写一个生成累加器的函数——一个接受数字 n 的函数,并返回另一个接受另一个数字 i 并返回 n 加 i 的函数。
(这是增加了,而不是加了。累加器必须进行累积。)
在 Common Lisp 中这将是
(defun foo (n) (lambda (i) (incf ni)))
在 Perl 5 中,
sub foo { my ($n) = @_; sub {$n += shift} }
它比 Lisp 版本具有更多元素,因为您必须在 Perl 中手动提取参数。
在 Smalltalk 中,代码比 Lisp 中的代码略长
foo: n |s| s := n. ^[:i| s := s+i. ]
因为虽然一般来说词汇变量可以起作用,但是您不能对参数进行赋值,所以您必须创建一个新的变量 s。
在 Javascript 中,这个例子又稍微长一些,因为 Javascript 保留了语句和表达式之间的区别,所以需要显式的返回语句来返回值:
function foo(n) { return function (i) { return n += i } }
(公平地说,Perl 也保留了这种区别,但是以典型的 Perl 方式处理它,即让您省略返回。)
如果您尝试将 Lisp/Perl/Smalltalk/Javascript 代码翻译成 Python,您会遇到一些限制。由于 Python 不完全支持词汇变量,因此您必须创建一个数据结构来保存 n 的值。尽管 Python 确实有函数数据类型,但没有文字表示(除非主体只是一个表达式),因此您需要创建一个命名函数来返回。这就是您最终得到的结果:
def foo(n): s = [n] def bar(i): s[0] += i return s[0] return bar
Python 用户可能会问,为什么他们不能直接写
def foo(n): return lambda i: return n += i
甚至
def foo(n): lambda i: n += i
我猜他们有一天可能会这么做。(但如果他们不想等待 Python 进化成 Lisp,他们也可以……)
在面向对象语言中,您可以在一定程度上模拟闭包(引用封闭作用域中定义的变量的函数),方法是定义一个类,该类具有一个方法和一个字段来替换封闭作用域中的每个变量。这使得程序员可以执行编译器在完全支持词法作用域的语言中执行的代码分析,如果多个函数引用同一个变量,它将不起作用,但在这种简单情况下就足够了。
Python 专家似乎同意这是用 Python 解决问题的首选方法,可以这样写
def foo(n): class acc: def __init__(self, s): self.s = s def inc(self, i): self.s += i return self.s return acc(n).inc
或者
class foo: def __init__(self, n): self.n = n def __call__(self, i): self.n += i return self.n
我之所以包含这些,是因为我不想让 Python 拥护者说我歪曲了语言,但在我看来,这两个版本都比第一个版本复杂。你在做同样的事情,设置一个单独的地方来保存累加器;它只是对象中的一个字段,而不是列表的头部。而且这些特殊的保留字段名称的使用,尤其是call ,似乎有点不合时宜。
在 Perl 和 Python 的竞争中,Python 黑客们声称 Python 似乎是 Perl 的更优雅的替代品,但这个案例表明,强大才是终极的优雅:Perl 程序更简单(元素更少),即使语法有点丑陋。
那么其他语言呢?在本次演讲中提到的其他语言中——Fortran、C、C++、Java 和 Visual Basic——尚不清楚是否真的可以解决这个问题。Ken Anderson 说,以下代码与 Java 中所能得到的最接近:
public interface Inttoint { public int call(int i); }
public static Inttoint foo(final int n) { return new Inttoint() { int s = n; public int call(int i) { s = s + i; return s; }}; }
这不符合规范,因为它只适用于整数。在与 Java 黑客进行过多次电子邮件交流后,我想说,编写一个行为与前面示例类似的正确多态版本介于非常尴尬和不可能之间。如果有人想写一个,我会非常好奇地想看看,但我个人已经超时了。
当然,说你无法用其他语言解决这个问题并不是真的。所有这些语言都是图灵等价的,严格来说,这意味着你可以用其中任何一种语言编写任何程序。那么你会怎么做呢?在极限情况下,用功能较弱的语言编写 Lisp 解释器。
这听起来像个笑话,但在大型编程项目中,这种现象经常以不同的程度发生,因此这种现象有一个名称,即 Greenspun 的第十条规则:
任何足够复杂的 C 或 Fortran 程序都包含一个临时的、非正式指定的、充满错误的、缓慢的 Common Lisp 实现。
如果你试图解决一个难题,问题不在于你是否使用足够强大的语言,而在于你是否 (a) 使用强大的语言,(b) 为某个语言编写事实上的解释器,或者 (c) 自己成为某个语言的人工编译器。我们在 Python 示例中已经看到这种情况开始发生,我们实际上是在模拟编译器为实现词法变量而生成的代码。
这种做法不仅很常见,而且已经制度化。例如,在面向对象世界中,您会听到很多关于“模式”的说法。我想知道这些模式是否有时是情况 (c)(即人工编译器)在工作的证据。当我在程序中看到模式时,我认为这是麻烦的征兆。程序的形状应该只反映它需要解决的问题。代码中的任何其他规律性至少对我来说都表明我使用的抽象不够强大——通常我手动生成需要编写的某些宏的扩展。
笔记
IBM 704 CPU 的大小与冰箱相当,但重量却要重得多。CPU 重 3150 磅,4K RAM 放在一个单独的盒子里,重达 4000 磅。Sub-Zero 690 是最大的家用冰箱之一,重达 656 磅。
史蒂夫·拉塞尔 (Steve Russell) 还于 1962 年编写了第一款(数字)计算机游戏《太空大战》。
如果你想骗取一个头发尖尖的老板让你用 Lisp 编写软件,你可以试着告诉他这是 XML。
以下是其他 Lisp 方言中的累加器生成器:
Scheme: (define (foo n) (lambda (i) (set! n (+ ni)) n)) Goo: (df foo (n) (op incf n _))) Arc: (def foo (n) [++ n _])
Erann Gat 讲述的有关 JPL 的“行业最佳实践”的悲伤故事启发了我去解决这个通常被误用的短语。
Peter Norvig 发现*《设计模式》*中的 23 种模式中有 16 种在 Lisp 中是“不可见的或者更简单”。
感谢许多回答我关于各种语言的问题和/或阅读本文草稿的人,包括 Ken Anderson、Trevor Blackwell、Erann Gat、Dan Giffin、Sarah Harlin、Jeremy Hylton、Robert Morris、Peter Norvig、Guy Steele 和 Anton van Straaten。他们对所表达的任何观点不承担任何责任。
有关的:
许多人对这次谈话做出了回应,因此我设立了一个额外的页面来处理他们提出的问题:回复:书呆子的复仇。
它还在LL1邮件列表中引发了广泛且常常有用的讨论。请特别参阅 Anton van Straaten 关于语义压缩的邮件。
LL1 上的一些邮件促使我尝试更深入地探讨简洁就是力量中的语言力量这一主题。
累加器生成器基准的大量规范实现都收集在其自己的页面上。