nerd 的反击
原文:Revenge of the Nerds
作者:Paul Graham 发表:2002-05
译者:Claude(baoyu-translate)
2002 年 5 月
“我们冲着 C++ 程序员去——我们设法把他们当中很多人拖了大约一半路程到 Lisp。” —— Guy Steele(Java 规范联合作者,Common Lisp 设计者之一)
软件这一行里有一场长期斗争,一边是“尖脑学者“,另一边是同样不容小觑的“尖头老板“——大家都知道“尖头老板“是谁吧?我猜技术圈里多数人不只认得这个漫画形象(《呆伯特》漫画里的平庸老板),还认得自己公司里被照着画的那个真人。
尖头老板奇迹般地集两种单独很常见、却很少同时出现的特质于一身:(a) 他对技术一无所知;(b) 他对技术意见极强。
比如说,假设你需要写一段软件。尖头老板对这段软件应该怎么工作毫无概念,连一种编程语言和另一种都分不清——但他就是知道你应该用什么语言写。没错,他认为你应该用 Java。
他为什么这么想?我们看一下尖头老板脑子里是怎么想的。他脑里大概是这样:Java 是个标准。我知道它是,因为我天天在媒体上读到。既然是标准,我用它就不会出事。这也意味着 Java 程序员永远不缺——所以我手下的这些程序员(莫名其妙地)哪天跑了,我也好换。
听上去也不是完全不讲理。但这一切都建立在一个没说出口的假设之上——而这个假设碰巧是错的。尖头老板相信:所有编程语言基本等价。如果这是真的,他就一点没说错——既然语言都等价,那当然用别人都在用的那门。
但所有语言并不等价——而我觉得我连进入“它们之间到底有什么差别“都不必,就能向你证明这一点。如果你 1992 年问尖头老板软件该用什么写,他会和今天一样不假思索地回答:“软件应该用 C++ 写。“但如果语言都等价,**为什么尖头老板的意见会变?**事实上——既然如此,为什么 Java 的设计者还要费力造一门新语言?
可以推断:你之所以造一门新语言,是因为你认为它比已有的好。事实上,Gosling(Java 之父)在第一份 Java 白皮书里说得很清楚——Java 的设计是为了修复 C++ 的一些问题。所以——语言并不等价。如果你顺着尖头老板脑子里的那条路径走到 Java、再顺着 Java 的历史走到它的源头,你最后手里抱着的,是一个与你出发时的假设互相矛盾的想法。
那么——谁是对的?James Gosling,还是尖头老板?毫不奇怪——Gosling 是对的。对于某些问题,有些语言确实比别的好。这就引出一些有意思的问题:Java 被设计成对某些问题比 C++ 好——什么问题?什么时候 Java 更好?什么时候 C++ 更好?是不是有些情况下别的语言比这两个都更好?
一旦你开始考虑这个问题,你就捅了马蜂窝。如果尖头老板必须以问题的全部复杂度来思考它,他的脑袋会爆炸。只要他认为所有语言等价,他要做的就是——挑那门势头最大的;而既然这件事更多是时尚问题、不是技术问题,连他也大概率能挑对。但如果语言有差,他突然就得解一个双联立方程——在两件他一无所知的事情之间找最优平衡:(1) 排前二十的语言里,对他需要解决的问题哪门最合适;(2) 每门各自能不能找到程序员、有没有库可用,等等。如果门那边是这么个东西,难怪他不愿意推开。
“所有编程语言等价“这个想法的劣势是——它不是真的。但它的优势是——它让你的人生简单得多。我觉得这正是它如此普遍的主要原因——它是一个让人舒服的想法。
我们之所以“知道“ 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(Lisp 之父)1958 年发现的,而流行的编程语言到现在才开始追上他那时候就开发出的想法。
这怎么可能?计算机技术不是变得很快吗?1958 年的电脑可是冰箱大小的庞然大物,处理力跟手表差不多——这么老的技术怎么可能甚至还相关,更别说优于最新的发展?
我告诉你为什么。因为 Lisp 本来就不是为做编程语言而设计的——至少不是我们今天这个意义上的“编程语言“。我们说“编程语言“,意思是用来告诉电脑做什么的东西。McCarthy 最终确实有意做一门这种意义上的编程语言;但我们最终拿到手里的 Lisp,基于的是他另一件事——一项理论练习,是为图灵机找一个更方便的替代品而做的努力。McCarthy 后来这样说:
另一种证明 Lisp 比图灵机更利落的方法,是写一个通用 Lisp 函数、并表明它比“通用图灵机“的描述更短、更易理解。这个 Lisp 函数就是 eval……,它计算一个 Lisp 表达式的值……。要写出 eval,必须发明一种把 Lisp 函数表示成 Lisp 数据的记号系统——这种记号是为论文目的而发明的,根本没想到它会被用来在实践中表达 Lisp 程序。
接下来发生的是——大约在 1958 年下半年的某个时点,McCarthy 的一位研究生 Steve Russell(McCarthy 的研究生,1958 年实现第一个 Lisp 解释器)盯着 eval 的这个定义,意识到——如果他把它翻译成机器语言,结果会是一个 Lisp 解释器。
这在当时是一件大惊喜。McCarthy 后来在一次访谈里这样说:
Steve Russell 说,看,要不我把这个 eval 编程实现一下吧——我对他说:“呵呵,你把理论和实践搞混了,这个 eval 是给人读的,不是给电脑算的。“但他还是去做了。也就是说,他把我论文里的 eval 编译成了 [IBM] 704 的机器码、修了 bug,然后把它当作一个 Lisp 解释器宣告——而它确实就是。所以从那一刻起,Lisp 就有了它今天这个形态的本质……
突然之间——大约几周里——McCarthy 发现自己的理论练习变成了一门实际的编程语言,而且是一门比他原本打算的更强大的语言。
所以“为什么这门 1950 年代的语言不算过时“的简短解释是:它不是技术、是数学——而数学不会变陈。把 Lisp 拿来对比的对象,不该是 1950 年代的硬件,而该是——比如——快速排序(Quicksort)算法:1960 年发现,至今仍是最快的通用排序。
还有另一门 1950 年代幸存下来的语言,Fortran——它代表了相反的语言设计路径。Lisp 是一项理论意外被变成了编程语言;Fortran 则是有意作为一门编程语言被开发出来的——但按今天的标准看,是非常底层的那一种。
Fortran I——1956 年开发出来的那门语言——和今天的 Fortran 是两种完全不同的动物。Fortran I 几乎就是带数学的汇编。在某些方面它甚至比晚近的汇编都弱——比如根本没有子程序,只有跳转。今天的 Fortran 反倒更像 Lisp,而非更像 Fortran I。
Lisp 和 Fortran 是两条独立进化树的主干——一条根植于数学,一条根植于机器架构。从那以来,这两棵树一直在汇合。Lisp 出生时强大,接下来 20 年里变得快;所谓“主流“语言出生时快,接下来 40 年里逐渐变得更强大——直到今天它们当中最先进的几门已经相当接近 Lisp。接近——但仍还差几样东西……
Lisp 不同在哪
Lisp 最初被开发出来时,体现了九个新想法。其中一些今天我们已经习以为常,另一些只在更先进的语言里看得到,还有两个,至今仍是 Lisp 独有。这九个想法,按主流采纳它们的先后顺序排:
-
条件判断(conditional)。条件判断就是 if-then-else 结构。今天我们觉得理所当然,但 Fortran I 没有它——它只有一种“条件 goto“,紧贴着底层机器指令。
-
函数类型(function type)。在 Lisp 里,函数和整数、字符串一样,是一种数据类型。它们有字面表示,可以存进变量,可以作为参数传递,等等。
-
递归。Lisp 是第一门支持它的编程语言。
-
动态类型。在 Lisp 里,所有变量本质上都是指针。有类型的是值,不是变量——给变量赋值或绑定,意味着拷贝指针,而不是拷贝指针所指的内容。
-
垃圾回收。
-
由表达式构成的程序。Lisp 程序是表达式的树——每一个都返回一个值。这与 Fortran 及其后多数语言形成对比——后者区分表达式和语句。
Fortran I 里有这种区分是很自然的,因为语句不能嵌套。所以你做数学时需要表达式,但让别的东西也返回值没意义——因为没有任何东西在等着接它。
这种限制随着块结构语言的出现消失了,但那时已经太晚——表达式与语句的区分已经根深蒂固。它从 Fortran 蔓延到 Algol,再蔓延到两者各自的后裔。
-
符号类型(symbol type)。符号本质上是指向“存在哈希表里的字符串“的指针。所以你可以比指针来测相等,而不必逐字符比。
-
用“由符号和常量构成的树“来表达代码的记号系统。
-
整门语言一直都在。读入时(read-time)、编译时、运行时之间没有真正的区分。你可以在读入时编译或运行代码,可以在编译时读入或运行代码,可以在运行时读入或编译代码。
读入时运行代码让用户可以重编程 Lisp 自己的语法;编译时运行代码是宏的基础;运行时编译是 Lisp 用作 Emacs 这类程序的扩展语言的基础;而运行时读入则使程序能用 s-表达式相互通讯——这一点最近被以 XML 的形式重新发明了一遍。
Lisp 刚出现时,这些想法和当时的日常编程实践相距甚远——后者大部分由 1950 年代末可用的硬件所决定。随着时间推移,“默认语言”——以一系列流行语言为载体——逐步朝 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 定义性的特质——McCarthy 把 Lisp 做成今天这个形态,正是为了让这件事成立。
语言在哪些地方真正有差别
那么假设 Lisp 确实代表了一个主流语言渐近逼近的极限——你应当真的用它来写软件吗?用一门没那么强的语言会亏多少?有时候不站在创新的最前沿是不是更明智?而流行本身在某种意义上不就是它自己的正当理由吗?尖头老板想要“一门容易招到程序员的语言“——比如这件事,他是不是就是对的?
当然,有些项目里“选什么语言“无关紧要。一般而言,应用越苛刻,强语言能给你的杠杆越大。但许多项目根本不苛刻。多数编程大概就是写些小胶水程序——而对这种小胶水程序,你可以用任何你已经熟悉、且对你需要做的事有好库的语言。如果你只是想把数据从一个 Windows 应用喂给另一个,那当然——用 Visual Basic 就行。
你也可以用 Lisp 写小胶水程序(我把它当桌面计算器用);但 Lisp 这类语言最大的胜场在另一端——你需要在激烈竞争中写出复杂的程序去解决难题的那一端。一个好例子是 ITA Software 授权给 Orbitz 的那个机票搜索程序。这帮人进入了一个被 Travelocity 和 Expedia 这两个巨大、根深蒂固的对手早已主导的市场——然后看上去就在技术上把它们羞辱了。
ITA 应用的核心,是一个 20 万行的 Common Lisp 程序——它搜索的可能性比对手们多了好几个数量级——而那些对手显然还在用大型机时代的编程技巧。(虽然 ITA 在某种意义上也在用一门大型机时代的编程语言。)我没看过 ITA 的任何代码,但据他们一位顶级黑客说他们用了很多宏——我听到这话不奇怪。
向心力
我不是说“用不常见技术没成本“。尖头老板担心这件事并不是完全错的——但因为他不理解风险所在,他倾向于放大风险。
我能想到使用不常见语言可能带来的三个问题:(1) 你的程序也许和别的语言写的程序不好协作;(2) 你能用的库会少一些;(3) 你招程序员可能会有麻烦。
每一条问题有多大?第一条的重要性,要看你是否控制整套系统。如果你在为远端用户的机器写软件、又要跑在某个 bug 不断的、闭源的操作系统之上(我不点名),那把应用用和操作系统同一种语言写,可能有好处。但如果你像 ITA 大概那样控制整套系统、且拥有所有部分的源代码,那你想用什么语言就用什么——出现任何不兼容你都能自己修。
在服务器端的应用里,你能用上最先进的技术——我认为这正是 Jonathan Erickson 所说的“编程语言文艺复兴“的主要原因。这正是为什么连 Perl 和 Python 这样的新语言也能被听到——我们听见这些语言不是因为有人用它们写 Windows 应用,而是因为有人在服务器上用它们。而随着软件离开桌面、走上服务器(这是连微软似乎都已经认命的未来),用“折中型“技术的压力会越来越小。
至于库——它们的重要性也取决于应用。对于不那么苛刻的问题,库的可用性能盖过语言本身的力量。盈亏点在哪?很难精确说,但不论它在哪,都在你称之为’应用’的东西的下方。如果一家公司认为自己是做软件的,且它正在写的东西会成为它的一款产品,那这件事大概会涉及好几位黑客、要花至少六个月——在这个规模的项目里,强语言的优势大概已经盖过现成库的便利。
尖头老板的第三个担心——招程序员难——我认为是一条红鲱鱼(误导性话题)。话说回来,你到底要招几位黑客?现在大家都该知道——软件最好由少于 10 人的团队开发。对任何人们听说过的语言,招到这个规模的黑客都不该有麻烦。如果你找不到 10 个 Lisp 黑客,那你公司大概就建错了城市。
事实上,选一门更强大的语言反而会减小你需要的团队规模——因为:(a) 如果你用一门更强的语言,你大概不需要那么多黑客;(b) 在更先进语言里工作的黑客,多半也更聪明。
我不是说你不会遭到很多“用’标准’技术“的压力。在 Viaweb(PG 1995 年创立的电商建站公司,今 Yahoo Store),我们用 Lisp 这件事让一些 VC(风险投资人)和潜在收购方挑了眉。但我们用通用 Intel 主机当服务器(而不是 Sun 那种“工业级“服务器)、用一种当时还冷门的开源 Unix 变种 FreeBSD(而不是 Windows NT 那种“真正的“商业 OS)、以及无视一个所谓的电商标准 SET(1990 年代的电子商务安全协议标准——今天连谁都不记得它了)——也让他们挑了眉。
你不能让“西装人“替你做技术决策。我们用 Lisp 这件事让一些潜在收购方有点警觉?是有那么点。但如果我们当时不用 Lisp,我们就根本写不出那段让他们想买我们的软件——在他们看来像是反常的事,其实是因果。
如果你创业,不要为了取悦 VC 或潜在收购方而设计你的产品。为了取悦用户而设计你的产品。如果你赢得了用户,其余的一切都会跟着来——而如果你没赢得用户,没人会在乎你的技术选择听起来有多“安心又正统“。
“平庸“的代价
用一门没那么强的语言会亏多少?这件事实际有数据。
衡量“力量“最方便的指标大概是代码长度。高级语言的意义就在于给你更大的抽象——打个比方,更大的砖头——这样你就不需要那么多砖来砌一面给定大小的墙。所以语言越强,程序就越短(当然不是字符意义上的短,而是不同的语法元素意义上的短)。
更强的语言怎么让你写出更短的程序?一种你可以采用的技巧(如果语言允许的话)叫自底向上编程:与其简单地用基础语言把你的应用写出来,不如在基础语言之上搭一门“用来写这种程序的语言“,然后再用它来写你的程序。两层加起来的代码可以比“用基础语言把整个程序写出来“短得多——事实上,多数压缩算法就是这么工作的。自底向上写出来的程序也更容易修改,因为很多情况下,那一层“语言“根本不必动。
代码长度重要,因为写程序的时间主要取决于它的长度。如果你的程序在另一种语言里有三倍长,写它就要花三倍时间——而你没法靠雇更多人绕过这件事——超过某个规模之后,新员工反而是净亏。Fred Brooks(《人月神话》作者)在他著名的 The Mythical Man-Month(《人月神话》)里描述过这个现象——而我所见的一切都倾向于印证他说的。
那么用 Lisp 写程序到底短多少?对 Lisp 对比 C,我听到的多数数字都在 7–10 倍。但 New Architect(软件杂志)最近一篇关于 ITA 的文章说“一行 Lisp 能替换 20 行 C“——而这篇文章满是 ITA 总裁的引语,我推测这个数字是从 ITA 那儿来的。如果是,那它有点分量——ITA 的软件也包含了大量 C 和 C++,他们是从经验上说的。
我猜这些倍数甚至不是常数。我觉得它们会随着你面对更难的问题而上升——也会随着你的程序员更聪明而上升。一个真正好的黑客能从更好的工具里榨出更多。
无论如何,作为曲线上的一个数据点——如果你要和 ITA 竞争、并选了 C 来写自己的软件,他们能比你快 20 倍地开发软件。如果你花一年做一个新功能,他们不到三周就能复制出来。反过来——如果他们花三个月做出了一个新东西,那要五年你才能做出同样的东西。
而你知道吗?这还是最好情况。当你说“代码长度比“时,你已经隐含假设——你真的能用那门弱语言把这个程序写出来。但事实上,程序员做事有上限。如果你试图用一门太底层的语言去解决一个难题,你会到达一个点——有太多东西要同时塞在脑子里,根本塞不下。
所以当我说 ITA 那位虚构的对手要花五年才能复制 ITA 用 Lisp 三个月就写出的东西时,我说的是一切顺利的情况下的五年。事实上,按多数公司的运作方式——任何“要五年“的开发项目,多半永远不会完成。
我承认这是个极端例子。ITA 的黑客似乎特别聪明,而 C 是相当底层的语言。但在一个有竞争的市场里,哪怕只是 2 倍或 3 倍的差距,也足以保证你永远落后。
一个配方
这是一种尖头老板根本不愿去想的可能性——所以多数他们也不去想。因为说到底——只要没人能证明是他的错,尖头老板并不介意公司被打得屁滚尿流。对他个人最安全的方案是——贴着群体的中心走。
在大型组织里,描述这种做法的短语是“业内最佳实践“。它的作用是为尖头老板挡责——如果他选了“业内最佳实践”,公司输了,他就不能被怪罪。他没选——是行业选的。
我相信这个词最初是用来描述会计方法之类的东西的。它大致意思是——别整奇葩。在会计里这大概是个好主意——“前沿“和“会计“放一起听上去就不对劲。但当你把这条标准搬进关于技术的决策时,你开始得到错误的答案。
技术常常应该在前沿。在编程语言上,正如 Erann Gat 指出的——“业内最佳实践“实际给你的不是最好的,而仅仅是平均的。当一个决定让你以“对手开发速度的零头“做事时,“最佳实践“是个用错的词。
所以这里我们有两件我认为非常有价值的信息——事实上,从我自己的经验我也知道。第一:语言在力量上有差别。第二:多数管理者刻意忽略这件事。这两件事合起来,字面意义上是一份赚钱配方。ITA 就是这份配方在行动中的例子。如果你想在一个软件生意里赢,那就找你能找到的最难的问题、用你能拿到的最强的语言、然后等你的对手们的尖头老板回归均值。
附录:力量
为了说明我所说的“编程语言相对力量“是什么意思,看下面这个问题。我们要写一个生成“累加器“的函数——它接收一个数字 n,返回一个函数;这个返回的函数接收另一个数字 i,返回n 增加 i 之后的值。
(注意,是增加 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 保留了“语句 vs 表达式“的区分,所以你需要显式 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__——在我看来有点 hack。
在 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 程序,都包含一份临时拼凑的、规格不正式的、满是 bug 的、缓慢的、Common Lisp 的一半的实现。
如果你试图解决一个难题,问题不是“你用不用一门足够强的语言“,而是你将 (a) 用一门强语言;(b) 为它实际上写一个解释器;还是 (c) 自己变成它的人肉编译器。我们看到这件事在 Python 例子里已经开始发生——我们事实上就是在手动模拟编译器为实现一个词法变量会生成的代码。
这种做法不只普遍,而且已被制度化。比如,在面向对象的世界里你会听到很多关于“模式“的事。我有时怀疑——这些模式,是不是其实就是 (c) 这种“人肉编译器在工作“的证据?我自己看到自己程序里有模式时,会把它视作一个麻烦的征兆。一段程序的形状应该只反映它要解决的那个问题——代码里任何别的规律性,对我至少是个征兆——说明我用的抽象不够强——往往说明我正在用手生成某个我应当写但还没写的宏的展开。
注释
-
IBM 704 的 CPU 大约冰箱大小——但重得多。CPU 重 3150 磅,4K 的内存放在另一个箱子里、再重 4000 磅。Sub-Zero 690(美国大型家用冰箱品牌),即最大的家用冰箱之一,重 656 磅。
-
Steve Russell 还在 1962 年写了第一款(数字)电脑游戏 Spacewar。
-
如果你想骗一个尖头老板让你用 Lisp 写软件,你可以试着告诉他这是 XML。
-
同一个累加器生成器在别的 Lisp 方言里:
Scheme: (define (foo n) (lambda (i) (set! n (+ n i)) 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。本文中表达的任何观点都与他们无关。