书呆子的复仇
想创办一家初创公司吗? 获得 Y Combinator 的投资。
2002年5月
“我们的目标是C++程序员。我们设法把他们中的许多人拖到了半路,让他们接近Lisp。”
—— Guy Steele,Java规范合著者
在软件行业中,始终存在着一场较量:一方是学究气的学者,另一方是同样强大的势力——尖发老板。大家都知道尖发老板是谁,对吧?我想科技界的大多数人不仅认识这个漫画人物,而且知道公司里那个以他为原型的真人。
尖发老板奇迹般地结合了两种品质,这两种品质单独来看很常见,但很少同时出现:(a) 他对技术一无所知,并且 (b) 他对技术有非常强烈的意见。
假设,例如,你需要编写一个软件。尖发老板不知道这个软件如何工作,也分不清编程语言,但他却知道你应该用什么语言来编写它。没错。他认为你应该用Java编写。
他为什么这么想?让我们来看看尖发老板的大脑内部。他的想法大概是这样的:Java是一个标准。我知道它一定是,因为我总在媒体上看到它。既然它是一个标准,我用它就不会惹上麻烦。那也意味着总会有很多Java程序员,所以如果现在为我工作的程序员辞职了——为我工作的程序员总是神秘地辞职——我可以轻松地替换他们。
嗯,这听起来并非毫无道理。但这全都基于一个未言明的假设,而这个假设被证明是错误的。尖发老板相信所有编程语言都大致等价。如果这是真的,那他就完全正确了。如果语言都是等价的,当然,就用大家都在用的语言好了。
但并非所有语言都是等价的,我想我能向您证明这一点,甚至不需要深入探讨它们之间的差异。如果你在1992年问尖发老板软件应该用什么语言编写,他会像今天一样毫不犹豫地回答:软件应该用C++编写。但如果所有语言都是等价的,尖发老板的意见为什么会改变呢?事实上,Java的开发者们为什么还要费心创造一门新语言呢?
可以推测,如果你创造一门新语言,那是因为你认为它在某些方面比人们已有的语言更好。事实上,Gosling在第一份Java白皮书中就明确表示,Java是为了解决C++的一些问题而设计的。所以你看:语言并非都是等价的。如果你顺着尖发老板的大脑追踪到Java,再通过Java的历史回溯到它的起源,你最终会得到一个与你最初假设相矛盾的观念。
那么,谁是对的?是James Gosling,还是尖发老板?毫不奇怪,Gosling是对的。对于某些问题,某些语言确实比其他语言更好。而且,你知道,这引发了一些有趣的问题。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是John McCarthy在1958年首次发现的,而流行的编程语言直到现在才赶上他当时提出的思想。
那么,这怎么可能呢?计算机技术不是变化非常快的东西吗?我的意思是,在1958年,计算机是冰箱大小的庞然大物,处理能力只有手表那么强。这么古老的技术怎么可能还有相关性,更不用说优于最新的发展了?
我会告诉你是怎么回事。这是因为Lisp并非真正被设计为一种编程语言,至少不是我们今天意义上的编程语言。我们今天所说的编程语言是我们用来告诉计算机做什么的东西。McCarthy最终确实打算开发一个这种意义上的编程语言,但我们最终得到的Lisp是基于他作为理论练习所做的另一项独立工作——试图定义一种比图灵机更方便的替代方案。正如McCarthy后来所说:
“另一种展示Lisp比图灵机更简洁的方法,是编写一个通用的Lisp函数,并证明它比通用图灵机的描述更简短、更易理解。这就是Lisp函数 eval……它计算一个Lisp表达式的值……编写 eval 需要发明一种将Lisp函数表示为Lisp数据的记号,这种记号是为了论文的目的而设计的,当时并没有想到它会用于实际中表达Lisp程序。”
接下来发生的事情是,在1958年末的某个时候,McCarthy的一名研究生Steve Russell看了这个 eval 的定义,意识到如果把它翻译成机器语言,结果将是一个Lisp解释器。
这在当时是个巨大的惊喜。以下是McCarthy后来在一次采访中所说的:
“Steve Russell说,看,我为什么不给这个 eval 编程呢……我对他说,嗬,嗬,你把理论和实践搞混了,这个 eval 是供阅读的,不是供计算的。但他还是继续做了。也就是说,他把我论文中的 eval 编译成了[IBM] 704机器代码,修复了错误,然后把它宣传为Lisp解释器,它确实是。所以从那时起,Lisp基本上就有了今天的形态……”
忽然间,我想就在几周内,McCarthy发现他的理论练习变成了一个实际的编程语言——而且比他原先设想的更强大。
所以,对于为什么这门1950年代的语言没有过时,简短的解释是:它不是技术,而是数学,而数学不会过时。与Lisp相提并论的,不应该是1950年代的硬件,而应该是,比如说,快速排序算法,它发现于1960年,至今仍是最快的通用排序方法。
还有一门从1950年代幸存下来的语言,Fortran,它代表了语言设计的相反方法。Lisp是一个意外变成编程语言的理论。Fortran则是有意作为编程语言开发的,但按照我们现在的标准,它是一种非常底层的语言。
Fortran I,这门1956年开发的语言,与现代Fortran是截然不同的东西。Fortran I基本上就是带数学运算的汇编语言。在某些方面,它甚至不如较新的汇编语言强大;例如,没有子程序,只有分支。现代Fortran可以说比Fortran I更接近Lisp了。
Lisp和Fortran是两个独立进化树的树干,一个植根于数学,一个植根于机器架构。这两棵树从此一直在汇聚。Lisp一开始就很强大,在接下来的二十年里变得快速。所谓的主流语言一开始很快,在接下来的四十年里逐渐变得更加强大,直到现在,其中最先进的那些已经相当接近Lisp了。接近了,但它们仍然缺少一些东西……
是什么让Lisp与众不同
在最初开发时,Lisp包含了九项新思想。其中一些我们现在认为理所当然,另一些只在更高级的语言中才能看到,还有两个至今仍是Lisp独有的。这九项思想,按被主流采用的顺序排列如下:
条件结构。条件结构是一种 if-then-else 结构。我们现在认为这些是理所当然的,但Fortran I没有。它只有一个基于底层机器指令的条件跳转。
函数类型。在Lisp中,函数是一种数据类型,就像整数或字符串一样。它们有字面表示法,可以存储在变量中,可以作为参数传递,等等。
递归。Lisp是第一个支持递归的编程语言。
动态类型。在Lisp中,所有变量实际上都是指针。值是拥有类型的,而不是变量,赋值或绑定变量意味着复制指针,而不是复制它们所指向的内容。
垃圾回收。
由表达式组成的程序。Lisp程序是表达式树,每个表达式都返回一个值。这与Fortran及后来的大多数语言形成对比,后者区分表达式和语句。
在Fortran I中,这种区分是很自然的,因为你不能嵌套语句。所以,虽然你需要表达式来进行数学运算,但没有理由让其他任何东西返回值,因为不可能有东西在等待它的返回值。
这种限制随着块结构语言的出现而消失了,但那时已经太晚了。表达式和语句之间的区别已经根深蒂固。它从Fortran传播到Algol,然后又传播到它们两者的后继者。
符号类型。符号本质上是指向哈希表中字符串的指针。所以你可以通过比较指针来测试相等性,而不是逐个字符比较。
使用符号和常量树表示代码的记号。
整个语言始终可用。读取时、编译时和运行时之间没有真正的区别。你可以在读取时编译或运行代码,在编译时读取或运行代码,在运行时读取或编译代码。
在读取时运行代码能让用户重新定义Lisp的语法;在编译时运行代码则是宏的基础;在运行时编译是Lisp在如Emacs等程序中作为扩展语言使用的根本;而在运行时读取则使程序能够通过s-表达式进行通信,这一概念最近被重新包装成了XML。
当Lisp初次登场时,这些理念与当时的常规编程实践相去甚远,那时的实践主要受限于1950年代末期的硬件条件。随着时间的推移,体现在一系列流行语言中的默认语言形态,已逐渐向Lisp靠拢。理念1至5现已广泛普及。理念6开始在主流领域显现。Python具备了理念7的某种形式,尽管似乎没有为其设计特定的语法。
至于理念8,可能是其中最为有趣的。理念8和9成为Lisp的一部分纯属偶然,因为史蒂夫·拉塞尔实现了麦卡锡本无意实现的东西。然而,正是这些理念最终促成了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应用,而是因为人们在服务器上使用它们。随着软件从桌面转向服务器(即便是微软似乎也已接受这一未来),使用中庸技术的压力将越来越小。
至于库,其重要性同样取决于应用。对于不太复杂的问题,库的可用性可能超过语言本身的性能。平衡点在哪里?很难精确说清,但无论在哪,都不会达到你认为称得上“应用”的程度。如果一家公司自认处于软件行业,正在编写作为其产品之一的应用,那很可能涉及多位黑客,至少需要六个月编写。在这样规模的项目中,强大语言的优势可能开始超过现有库的便利性。
“尖发老板”的第三个担忧——招募程序员的难度,我认为是个伪命题。毕竟,你需要雇佣多少黑客呢?现在我们都知道,软件最好由少于十人的团队开发。对于任何听说过其名的语言,要招募到这个规模的黑客应该都不难。如果你连十个Lisp黑客都找不到,那你的公司很可能选错了城市,不适合软件开发。
实际上,选择更强大的语言可能会缩减所需团队规模,因为(a)使用更强大的语言,可能不需要那么多黑客;(b)使用更先进语言的黑客往往更聪明。
我并不是说你不会受到采用“标准”技术的巨大压力。在Viaweb(现为Yahoo Store),我们因使用Lisp而在风投和潜在收购方中引来侧目。但我们还因使用通用Intel盒子作为服务器而非“工业级”的Sun服务器,使用当时鲜为人知的开源Unix变种FreeBSD而非Windows NT等真正的商业操作系统,无视当时号称电子商务标准、现已无人记得的SET等等,同样招致了疑惑。
你不能让那些“西装革履”的人替你决定技术问题。我们使用Lisp是否让某些潜在收购者感到不安?确实有,但微乎其微。如果没有用Lisp,我们就不可能写出让他们想要收购我们的软件。在他们看来异常的选择,实际上正是因果所在。
如果你创立一家初创公司,不要为了取悦风投或潜在收购方而设计产品。为满足用户而设计产品。 赢得了用户,其他一切自然水到渠成。反之,若未能赢得用户,没人会在意你的技术选择是多么正统无害。
平庸的代价
使用较弱的语言,你会损失多少?其实有一些相关数据可供参考。
衡量能力最便捷的指标可能是代码长度。高级语言的目的在于提供更大的抽象——好比更大的砖块,这样砌一面特定大小的墙就不需要那么多砖。因此,语言越强大,程序越简短(当然,不仅指字符数,更指不同元素的数量)。
更强大的语言如何让你写出更短的程序?如果语言允许,你可以使用一种名为“自底向上编程”的技术。它不是直接在基础语言中编写整个应用,而是在基础语言之上构建一个专门用于这类程序的编程语言,再用它来编写你的程序。这样组合起来的代码可能比全部用基础语言编写短得多——实际上,这正是多数压缩算法的工作原理。自底向上的程序也更容易修改,因为很多时候语言层根本无需变动。
代码长度至关重要,因为编写程序的时间主要取决于其长度。如果同一程序用另一种语言编写长度是原来三倍,编写时间也会是原来的三倍——而且雇佣更多人并不能解决这个问题,因为超过一定规模,新增人手反而会降低效率。弗雷德·布鲁克斯在其名著《人月神话》中描述了这一现象,我所见的一切都倾向于证实他的观点。
那么,用Lisp编写的程序能短多少呢?例如,我听到的多数关于Lisp与C语言对比的数字,大约在7到10倍之间。但最近《新架构师》杂志上一篇关于ITA的文章声称“一行Lisp可替代20行C语言”,鉴于这篇文章引用了许多ITA总裁的话,我认为这数字来源于ITA。若真如此,我们就可以采信;ITA的软件中包含大量C和C++代码,也使用Lisp,因此他们是有实际经验的。
我猜想这些倍数甚至并非恒定。我认为当面对更棘手的问题或程序员更聪明时,倍数会增大。真正优秀的黑客能从优秀工具中榨取更多价值。
无论如何,作为曲线上的一个数据点,如果你要与ITA竞争并选择用C编写软件,他们将能以比你快20倍的速度开发。如果你花一年时间开发一项新功能,他们可能用不到三周就能复制出来。而如果他们只花三个月开发新东西,你可能需要五年才能追上。
而且你知道吗?这还是最乐观的情况。当你谈论代码长度比时,你隐含假设了能用较弱语言编写该程序。但事实上,程序员所能做的有极限。如果你试图用层次过低的语言解决难题,会到达一个无法同时兼顾过多的临界点。
所以,当我说如果一切顺利的话,ITA 的虚构竞争对手需要五年时间才能复制 ITA 用 Lisp 三个月就能写出的东西,我的意思是,即使一切顺利也需要五年。事实上,在大多数公司的运作方式下,任何需要五年的开发项目很可能永远也完不成。
我承认这是一个极端的例子。ITA 的黑客似乎格外聪明,而 C 语言又是一种相当底层的语言。但在竞争激烈的市场中,即使是二比一或三比一的差距,也足以保证你永远落后。
一剂良方
这是那种“尖头发老板”们连想都不愿想的情况。所以大多数老板都选择不想。因为,要知道,归根结底,只要没人能证明是他的错,尖头发老板并不介意公司被打得落花流水。 对他来说,最安全的个人策略就是紧紧跟随大部队。
在大型组织内部,用来描述这种方法的口号是“行业最佳实践”。 其目的是让尖头发老板免于承担责任:如果他选择了某种“行业最佳实践”,而公司失败了,那也不能怪他。不是他选择的,是行业选择的。
我相信这个词最初是用来描述会计方法之类的。它的大致意思是别做任何奇怪的事。在会计领域,这或许是个好主意。“前沿”和“会计”这两个词放在一起确实不搭。但当你在技术决策中引入这个标准时,你就会开始得出错误的答案。
技术常常应该是前沿的。正如 Erann Gat 所指出的,在编程语言领域,“行业最佳实践”带给你的不是最好的,而仅仅是平均水平的。当一个决策导致你以比更具进取心的竞争对手慢得多的速度开发软件时,“最佳实践”就是个用词不当的说法。
所以我们这里有两条我认为非常有价值的信息。事实上,我是根据亲身经历知道的。 第一,语言在能力上存在差异。 第二,大多数管理者刻意忽视这一点。 这两条事实加在一起,简直就是赚钱的秘诀。ITA 就是这一秘诀付诸实践的例证。 如果你想在软件业务中取胜,那就去接手你能找到的最难的问题,使用你所能得到的最强大的语言,然后等着你的竞争对手的尖头发老板们回归平庸。
附录:能力
为了说明我所指的程序设计语言相对能力,请考虑以下问题。 我们想要编写一个生成累加器的函数——即一个接受数字 n,返回一个接受另一个数字 i 并返回 n 加上 i(即 n 递增 i)的函数的函数。
(是递增,不是加。累加器必须累加。)
在 Common Lisp 中,这将是
(defun foo (n) (lambda (i) (incf n i)))
而在 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 保留了语句和表达式之间的区别,所以你需要显式的 return 语句来返回值:
function foo(n) { return function (i) { return n += i } }
(公平地说,Perl 也保留了这种区别,但它以典型的 Perl 方式处理,即允许你省略 return。)
如果你尝试把 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 解释器。
这听起来像个玩笑,但在大型编程项目中,这种情况发生得如此频繁且程度各异,以至于这种现象有一个专门的名字,即格林斯潘第十定律:
任何足够复杂的 C 或 Fortran 程序都包含一个临时的、非正式规定的、充满错误的、缓慢实现的 Common Lisp 的半个副本。
如果你试图解决一个难题,问题不在于你是否会使用足够强大的语言,而在于你是否会(a)使用强大的语言,(b)为一个语言编写一个事实上的解释器,或者(c)自己成为它的一个人肉编译器。我们在 Python 示例中已经看到这种情况开始发生,我们实际上是在模拟编译器为实现词法变量而生成的代码。
这种做法不仅普遍,而且已经制度化。例如,在面向对象的世界里,你会听到很多关于“模式”的说法。 我想知道这些模式是否有时不是(c)情况——人肉编译器——在工作中的证据。当我在我的程序中看到模式时,我认为这是麻烦的迹象。程序的形状应该只反映它需要解决的问题。 代码中任何其他的规律性,至少对我来说,都表明我正在使用的抽象不够强大——通常是我在手动展开某个我需要编写的宏。
注释
[1] IBM 704 CPU 大约有冰箱那么大,但重得多。CPU 重 3150 磅,而 4K 内存则装在另一个重达 4000 磅的独立机箱中。Sub-Zero 690,最大的家用冰箱之一,重 656 磅。
[2] Steve Russell 还在 1962 年编写了第一个(数字)计算机游戏,Spacewar。
[3] 如果你想骗过尖头发老板,让他允许你用 Lisp 编写软件,你可以试着告诉他这是 XML。
[4] 这里是其他 Lisp 方言中的累加器生成器:
Scheme: (define (foo n) (lambda (i) (set! n (+ n i)) n)) Goo: (df foo (n) (op incf n ))) Arc: (def foo (n) [++ n ])
[5] Erann Gat 在 JPL 关于“行业最佳实践”的悲惨故事启发我探讨了这个被普遍误用的短语。
[6] Peter Norvig 发现《设计模式》中的 23 个模式中有 16 个在 Lisp 中是“不可见或更简单的”。
[7] 感谢许多回答了我关于各种语言问题以及/或阅读了本文草稿的人,包括 Ken Anderson, Trevor Blackwell, Erann Gat, Dan Giffin, Sarah Harlin, Jeremy Hylton, Robert Morris, Peter Norvig, Guy Steele 和 Anton van Straaten。 他们对文中表达的任何观点概不负责。
相关阅读:
许多人回应了这次演讲,所以我建立了一个额外的页面来处理他们提出的问题:Re: Revenge of the Nerds。
它还在 LL1 邮件列表上引发了一场广泛且常有价值的讨论。特别请参阅 Anton van Straaten 关于语义压缩的邮件。
LL1 上的一些邮件促使我在《简洁即力量》中尝试更深入地探讨语言能力这一主题。
一组更完整的累加器生成器基准测试的规范实现被收集在它们自己的页面上。
日语翻译,西班牙语翻译,中文翻译