活了二十多年,没能为祖国、为人民做点什么,每思及此,伤心欲绝 !

黑客与画家 13.书呆子的复仇

书籍 柯广 632℃

13.书呆子的复仇

  软件业有一场永不停息的战斗,书生气的开发者与官僚主义的经理之间总是发生冲突。大家应该都看过漫画《呆伯特》,熟悉里面那个发型高耸的经理^。我想,技术行业的大部分人对这个角色都过目难忘,因为在他们的公司里就有这个角色的原型。

^「这篇文章发上网后,我收到了一封显然发自肺腑的电子邮件,开头是这样写的:发型高耸?难道有谁的头发不是向上耸的吗?如果你只能想出这种词去悔辱你的经理,那么你们这些人活该被称为“书呆子”。」

  那些经理奇迹般地同时具备了两种很常见但很难结合在一起的特点:(a)对技术一无所知;(b)对技术有强烈的个人观点。

  举例来说,假设你需要写一个软件。你的经理根本不懂这个软件的运作机制,也不知道各种编程语言有什么区别。但是,他竟然明确要求你一定要使用某一种语言进行开发。没错,他就是要求你一定要用Java语言。

  为什么他会提出这种要求?让我们看看他脑袋里是怎么想的。他的想法无非就是,Java是业界的标准。我知道肯定如此,因为媒体对此有铺天盖地的报道。既然它是标准,那么使用它就不会错。另外,这也意味着人才市场上肯定有无数Java程序员,即使现在为我打工的这批人都辞职了(真奇怪,这种事情总是不断发生),我也能够轻易地找到替代者。

  嗯,这听起来也不无道理。但是,它的前提是一个没有说出口的假设,而这个假设实际上是错的。你的经理相信所有编程语言的功能都差不多,可以互相替代。如果这种想法是对的,那么他要求你用Java编程就很合理了。反正编程语言之间没有区别,那么就用大家都在用的那种语言吧。

  但是,编程语言是不一样的。就算不探讨各种语言之间的具体区别,我也能向你证明这一点。回到1992年,如果你问经理使用什么语言开发软件。他会像今天一样毫不迟疑地回答说C++。如果所有编程语言都一样,为什么答案变了?进一步说,为什么Java语言的设计者要如此麻烦地去创造一种新语言呢?

  一般来说,如果你动手创造一种新语言,那是因为你觉得它在某些方面会优于现有的语言。Java语言之父詹姆斯·戈斯林在第一份《Java白皮书》中说得很清楚,之所以要设计Java,就是想解决C++的一些弱点。所以结论就是,各种编程语言的编程能力是不相同的。如果你接受你的经理的假设,然后一路追溯到Java语言的源头,就会得到与他的假设完全不同的结果。

  到底谁对?戈斯林还是你的经理?结果当然是意料之中的,戈斯林是正确的。某些情况下,一些语言就是比另一些语言更出色。可是这样一说又导致了另外的问题。C++不适合解决某些难题,所以Java才被设计出来。那么,什么情况下应该使用Java,什么情况下应该使用C++呢?会不会某些情况下其他语言比它们更合适呢?

  一旦你开始思考这个问题,就会发现它非常棘手。如果你的经理被迫去想这个问题,当他看到它的复杂性时,脑袋恐怕都会爆炸。如果所有语言真的都一样,那么他只需选择一种看上去获得大部分人拥戴的语言就可以了,因为这实际上是一种流行风尚,而不是技术问题,所以即使像你的经理这样对技术无知的人也有可能轻松得到正确答案。但是,如果语言各有不同,你的经理就会突然发现,有两个互相关联的方程,他必须找到一个能够同时满足两个方程的最佳解,而最要命的却是他对此根本一无所知。第一个方程是找到(相对于要解决的问题)能够适用20年左右的最佳语言,第二个方程是(为这种语言)找到合适的程序员、函数库的机会有多大。如果假定所有语言都不同,就会遇到这种苦苦求解的情况,所以难怪你的经理不愿意接受这个假设了。

  认为所有语言都一样的看法的缺点是自欺欺人,但是优点是可以使许多事情变得很简单。我想这就是为什么它被广泛接受的主要原因。它是一个令人舒服的想法。

  大家都觉得Java—定有过人之处,因为它是一种很酷的新兴编程语言。但是真的如此吗?如果你站在远处观察编程语言的世界,似乎Java就是最新的东西。(如果你站得足够远,那么你看到的所有东西就是Sun公司出钱制作的大型霓虹广告牌。)但是,如果你靠近观察这个世界,就会发现不同的人对“酷”的理解是不一样的。在黑客圏子里,Perl被公认比Java酷得多。黑客社区网站Slashdot就是用Perl开发的。我估计你不可能看到黑客愿意使用Java的JSP技术开发网站。可是,还有一种更新的语言叫做Python,它的使用者往往看不起Perl。另一些人则认为Ruby语言是取代Python的最佳选择。

  当你按照Java、Perl,Python,Ruby这样的顺序观察这些语言,你会发现一个有趣的结果。至少,如果你是一个Lisp黑客,你就看得出来,排在越后面的语言越像Lisp。Python语言模仿Lisp,甚至把许多Lisp黑客认为属于设计错误的功能也一起模仿了。至于Ruby语言,如果回到1975年,你声称它是一种有着自己句法的Lisp方言,没有人会提出反对意见。编程语言现在的发展不过刚刚赶上1958年Lisp语言的水平。

朝着数学的方法发展

  1958年,约翰·麦卡锡第一个提出了Lisp语言。我认为,当前最流行的编程语言不过只是实现了他在1958年的想法而已。

  这怎么可能呢?计算机技术的发展不是日新月异吗?1958年的计算机的运算能力还不如今天的电子表,而体积却大得像冰箱^。那时的技术怎么可能超过今天的水平呢?

^「IBM 704型计算机的CPU就像冰箱一样大,并且重得多(1429千克)。4K大小的RAM则装在另外一个箱子里,重达1800千克。相比之下,Sub-zero 690是最大的家用冰箱型号之一,重量还不到300千克。 」

图13-1 IBM 704,美国劳伦斯利弗莫尔国家实验室,1956年

  让我告诉你原因。这是因为设计者本来没打算把Lisp设计成编程语言,至少不是我们现在意义上的编程语言。我们今天所说的编程语言指的是用来告诉计算机怎么做的一种工具。麦卡锡最后确实有意开发这种意义上的编程语言,但是实际上他做出来的Lisp却是完全不同的一种东西,语言的基础是他的一种理论演算,他想用更简洁的方式定义图灵机。正如他后来所说:

Lisp比图灵机表达起来更简洁。证明这一点的一种方法就是写一个Lisp通用函数,证明它比图灵机的一般性描述更短、更易僅。这个Lisp函数就是eval……它用来计算Lisp表达式的值……。编写eval函数需要发明一种表示法,能够把Lisp函数表示成Lisp数据。设计这种书写法完全是为了满足论文写作的需要。(我)根本没有想过用它来编写Lisp程序并在计算机上运行。

图13-2 书呆子之王约翰·麦卡锡

  1958年年底,麦卡锡的一个学生史蒂夫·拉塞尔^看到了eval函数的定义,意识到如果把它翻译成机器语言,就可以把Lisp解释器做出来。这在当时是非常令人吃惊的事。麦卡锡后来回忆:

拉塞尔对我说:“我想把eval编成程序……”我告诉他,别把理论和实践混淆,eval只是用来读的,不是用来做计算的。但是他执意要做,并且还真的做出来了。就是说,他把我论文中的eval编译成了[IBM] 704计算机的机器码,修正了bug,然后对外宣布做出了Lisp语言的一种解释器,这倒没有说错,确实如此。所以,从那个时候开始,Lisp语言就基本上是它现在的样子了……

^「Steve Russell,也是历史上第一个电脑游戏的作者,1962年他写了《太空大战》(Spacewar)。」

  这样一下子,就在几个星期之内,麦卡锡发现他的理论演算变成了一种实际的编程语言,而且出乎意料地强大。

  由此也就得出了20世纪50年代的编程语言到现在还没有过时的原因。简单说,因为这种语言本质上不是一种技术,而是数学。数学是不会过时的。你不应该把Lisp语言与50年代的硬件联系在一起,而是应该把它与快速排序(Quicksort)算法进行类比。这种算法是1960年提出的,至今仍然是最快的通用排序方法。

  Fortran语言也是20世纪50年代出现的,并且一直使用至今。它代表了语言设计的一种完全不同的方向。Lisp语言是无意中从纯理论发展为编程语言的,而Fortran从一开始就是作为编程语言设计出来的。但是,今天我们把Lisp看成高级语言,而把Fortran看成一种相当低层次的语言。

  1956年Fortran刚诞生的时候,叫做Fortran I,与今天的Fortran语言差别极大。Fortran I实际上是汇编语言加上数学,在某些方面还不如今天的汇编语言强大。比如,它没有子例程,只有分支跳转结构(branch)。今天的Fortran语言可以说更接近Lisp而不是Fortran I。

  Lisp和Fortran代表了编程语言发展的两大方向。前者的基础是数学,后者的基础是硬件架构。从那时起,这两大方向一直在互相靠拢。Lisp语言刚设计出来的时候就很强大,接下来的二十年它提高了运行速度。而那些所谓的主流语言把更快的运行速度作为设计的出发点,然后再用四十多年的时间一步步变得更强大。直到今天,最高级的主流语言也只是刚刚接近Lisp的水平。虽然已经很接近了,但还是没有Lisp那样强大。

为什么Lisp语言很特别

  Lisp语言诞生的时候就包含了9种新思想。其中一些我们今天已经习以为常,另一些则刚刚在其他高级语言中出现,至今还有2种是Lisp独有的。按照被大众接受的程度,这9种思想依次如下排列。

  (1) 条件结构(即if-then-else结构)。现在大家都觉得这是理所当然的,但是Fortran I就没有这个结构,它只有基于底层机器指令的goto结构。

  (2) 函数也是一种数据类型。在Lisp语言中,函数与整数或字符串一样,也属于数据类型的一种。它有自己的字面表示形式(literal representation),能够存储在变量中,也能当作参数传递。一种数据类型应该有的功能,它都有。

  (3) 递归。Lisp是第一种支持递归函数的高级语言^。

^「Lisp语言的许多特性(比如,把程序写成列表形式以及实现某种形式的递归)都在20世纪50年代的IPL-V语言中出现过。但是,IPL-V更像是汇编语言,它的程序中充满了操作码/地址对。参见Alien Newell等人编著的《IPL-V语言操作手册》(Information Processing Language-V Manual),Prentice-Hail,1961年出版。」

  (4) 变量的动态类型。在Lisp语言中,所有变量实际上都是指针,所指向的值有类型之分,而变量本身没有。复制变量就相当于复制指针,而不是复制它们指向的数据。

  (5) 垃圾回收机制。

  (6) 程序由表达式组成。Lisp程序是一些表达式树的集合,每个表达式都返回一个值。这与Fortran和大多数后来的语言都截然不同,它们的程序由表达式和语句组成。

  区分表达式和语句在Fortran I中是很自然的,因为它不支持语句嵌套。所以,如果你需要用数学式子计算一个值,那就只有用表达式返回这个值,没有其他语法结构可用,否则就无法处理这个值。

  后来,新的编程语言支持块结构,这种限制当然也就不存在了。但是为时已晚,表达式和语句的区分已经根深蒂固。它从Fortran扩散到Algol语言,接着又扩散到它们两者的后继语言。

  (7) 符号类型。符号实际上是一种指针,指向存储在散列表中的字符串。所以,比较两个符号是否相等,只要看它们的指针是否一样就行了,不用逐个字符地比较。

  (8) 代码使用符号和常量组成的树形表示法。

  (9) 无论什么时候,整个语言都是可用的。Lisp并不真正区分读取期、编译期和运行期。你可以在读取期编译或运行代码,也可以在编译期读取或运行代码,还可以在运行期读取或者编译代码。

  在读取期运行代码,使得用户可以重新调整(reprogram)Lisp的语法;在编译期运行代码,则是Lisp宏的工作基础;在运行期编译代码,使得Lisp可以在Emacs这样的程序中充当扩展语言(extension language);在运行期读取代码,使得程序之间可以用S表达式(S-expression)通信,近来XML格式的出现使得这个概念被重新“发明”出来了^。

「如果你不想让经理发现你正在使用Lisp编程,你可以告诉他你用的是XML。」

  Lisp语言刚出现的时候,这些思想与其他编程语言大相径庭,后者的设计思想主要由50年代后期的硬件决定。随着时间流逝,流行的编程语言不断更新换代,语言设计思想逐渐向Lisp靠拢。思想(1)到思想(5)已经被广泛接受,思想(6)开始在主流编程语言中出现,思想(7)在Python语言中有所实现,不过似乎没有专用的语法。

  思想(8)可能是最有意思的一点。它与思想(9)只是由于偶然原因才成为Lisp语言的一部分,因为它们不属于麦卡锡的原始构想,是由拉塞尔自行添加的。它们从此使得Lisp语言看上去很古怪,但也成为了这种语言最独一无二的特点。说Lisp语言古怪倒不是因为它的语法很古怪,而是因为它根本没有语法,程序直接以解析树(parse tree)的形式表达出来。在其他语言中,这种形式只是经过解析在后台产生,但是Lisp直接采用它作为表达形式。它由列表构成,而列表则是Lisp的基本数据结构。

  用一门语言自己的数据结构来表达该语言是非常强大的功能。思想(8)和思想(9),意味着你可以写出一种能够自己编程的程序。这可能听起来很怪异,但是对于Lisp语言却是再普通不过。最常用的做法就是使用宏。

  术语“宏”在Lisp语言中的意思与其他语言中的不一样。Lisp宏无所不包,它既可能是某样表达式的缩略形式,也可能是一种新语言的编译器。无论是想真正理解Lisp语言,还是只想拓宽编程视野,最好都学学宏。

  就我所知,宏(采用Lisp语言的定义)目前仍然是Lisp独有的。一个原因是为了使用宏,你大概不得不让你的语言看上去像Lisp—样古怪。另一个可能的原因是,如果你想为自己的语言添上这种终极武器,你从此就不能声称自己发明了新语言,只能说发明了一种Lisp的新方言。

  我把这件事当作笑话说出来,但是事实就是如此。如果你创造了一种新语言,其中有car、cdr、cons、quote、cond、atom、eq这样的功能,还有一种把函数写成列表的表示方法,那么在它们的基础上完全可以推导出Lisp语言的所有其他部分。事实上,Lisp语言就是这样定义的,麦卡锡把语言设计成这个样子就是为了让这种推导成为可能。

语言优势真正体现的地方

  就算Lisp确实代表了目前主流编程语言不断靠近的一个方向,这是否意味着你就应该用它编程呢?如果使用一种不是如此强大的语言,你又会有多少损失呢?有时不采用最尖端的技术不也是一种明智的选择吗?这么多人使用主流编程语言,这本身不也说明那些语言有可取之处吗?举例来说,你的经理不正是希望使用一种很容易雇到程序员的语言吗?

  另一方面,许多项目是无所谓选择哪一种编程语言,反正不同的语言都能完成工作。一般来说,条件越苛刻的项目,强大的编程语言就越能发挥作用。但是,无数的项目根本没有苛刻条件的限制。大多数的编程任务可能只要写一些很小“胶水程序”,然后再把这些小程序连起来就行了。你可以用自己熟悉的编程语言或者用对于特定项目来说有着最强大函数库的语言来写这些“胶水程序”。如果你只是需要在Windows应用程序之间传递数据,使用Visual Basic照样能达到目的。

  你也可以使用Lisp语言编写这些小程序(我用它写了桌面计算器),但是Lisp的最大优势体现在编程任务的另一端,就是在激烈竞争的条件下开发那些解决困难问题的复杂程序。ITA软件公司为Orbitz旅行社开发的飞机票价搜索程序就是一个很好的例子。网络订票市场很难迸入,因为它已经被两大巨头(Travelocity和Expedia)牢牢控制了,但是ITA的软件性能看上去使得那两家公司的软件顿时相形见绌。

  ITA的软件的核心是一个20万行的Common Lisp程序,它的捜索能力比竞争对手高出许多个数量级。那些竞争对手依然使用大型机时代的编程方法。我没有看过ITA的软件源码,但是据一个为它工作的顶尖黑客说,他们使用了大量的宏。果然不出我所料。

向心力

  我承认,使用一种不常用的技术也有代价。你的经理担心这一点并不是完全没有道理的。但是,因为他不懂风险出在什么地方,所以往往把风险夸大了。

  使用一种不常见的语言会出现的问题我想到了三个:你的程序可能无法很好地与使用其他语言写的程序协同工作;你可能找不到很多函数库;你可能不容易雇到程序员。

  它们有多严重?第一个问题取决于你是否控制整个系统。如果你的软件运行在客户的机器上,而客户又使用一个到处都是bug的专有操作系统(我可没提操作系统的名字),那么使用那个操作系统的开发语言可能会给你带来优势。但是,如果你控制整个系统,并且还有各个组成部分的源码(正如我推测ITA就是这种情况),那么你就能使用任何你想用的语言。如果出现不兼容的情况,你自己就能动手解决。

  把软件运行在服务器端就可以没有顾忌地使用最先进的技术。乔纳森·埃里克森说现在是“编程语言的文艺复兴时期”,我想最大的原因就是有了服务器端软件。这也能解释为什么像Perl和Python这样的新语言会流行起来,它们之所以流行不是因为人们使用它们开发Windows应用程序,而是因为人们在服务器上使用它们。随着软件从桌面端向服务器端转移(连微软公司都看出这是未来的趋势),逼迫你使用某一种语言的限制将越来越少。

  至于第二个问题,函数库的重要性也取决于你的应用程序。对于那些条件不苛刻的应用,有没有一个好的函数库比语言本身的能力更重要。那么到底应该怎么选择语言?是根据函数库,还是根据语言本身的能力?很难确切地找出一条清楚的规则,但是无论哪种情况,你都必须考虑到你开发的应用程序的特点。如果你是一家软件公司,你开发的程序打算拿到市场上销售,那么这个程序可能会耗费好几个优秀程序员至少6个月的时间。为一个这样规模的项目选择编程语言,语言本身要有强大的编程能力可能就是最重要的考虑因素,比是否有方便的函数库更重要。第三个问题是你的经理担忧雇不到程序员,我认为这根本就是混淆视听。说实话,你究竟想雇用多少个黑客?到目前为止,大家公认少于10个人的团队最适合开发软件。雇用这样规模的开发团队,只要使用的不是无人知道的语言,应该都不会遇到很大麻烦。如果你无法找到10个Lisp黑客,那么你可能选错了创立软件公司的城市。

  事实上,选择更强大的编程语言会减少所需要的开发人员数量。因为:(a)如果你使用的语言很强大,可能会减少一些编程的工作量,也就不需要那么多黑客了;(b)使用更高级语言的黑客可能比别的程序员更聪明。

  我不是说外界因素对你没有影响,肯定还是会有很大压力,逼迫你使用公认的“标准”技术。Viaweb创业期间,很多风险投资商和潜在的并购方看到我们使用Lisp语言都感到很吃惊和不以为然。但是,我们让他们吃惊的还不止这一个地方,我们使用普通的兼容机充当服务器,而不是“企业级”的Sun服务器;我们使用那时还默默无闻的开源Unix系统FreeBSD,而不是流行的商业操作系统Windows NT,我们也没有采用SET(Secure Electronic Transaction,安全电子交易),它被认为将成为电子商务标准,而实际上现在没人记得它。诸如此类的事情还有很多。你不能让那些衣冠楚楚、西装革履的家伙替你做技术决策。潜在的并购方有没有对我们使用Lisp语言感到很难接受?稍微有一点吧,但是如果我们不使用Lisp,我们就根本写不出现在的软件,也就不会有人想收购我们。他们眼中不正常的事情恰恰就是使得这一切发生的原因所在。如果你创业的话,千万不要为了取悦风险投资商或潜在并购方而设计你的产品。让用户感到满意才是你的设计方向。只要赢得用户,其他事情就会接踵而来。如果没有用户,谁会关心你选择的“正统”技术是多么令人放心。

随大流的代价

  使用一种不强大的语言,你的损失有多大?实际上有一些现成的数据可以说明这个问题。

  衡量语言的编程能力的最简单方法可能就是看代码数量。所谓高级语言,就是能够提供更强大抽象能力的语言,从某种意义上,就像能够提供更大的砖头,所以砌墙的时候用到的砖头数量就变少了。因此,语言的编程能力越强大,写出来的程序就越短(当然不是指字符数量,而是指独立的语法单位)。

  强大的编程语言如何让你写出更短的程序?一个技巧就是(在语言允许的前提下)使用“自下而上”(bottom-up)的编程方法。你不是用基础语言(base language)开发应用程序,而是在基础语言之上先构建一种你自己的语言,然后再用后者开发应用程序。这样写出来的代码会比直接用基础语言开发出来的短得多。实际上,大多数压缩算法也是这样运作的。“自下而上”的编程往往也便于修改,因为许多时候你自己添加的中间层根本不需要变化,你只需要修改前端逻辑就可以了。

  代码的数量很重要,因为开发一个程序所耗费的时间主要取决于程序的长度。对于同一个软件,如果用一种语言写出来的代码比用另一种语言长三倍,这意味着你开发它耗费的时间也会多三倍。而且即使多雇人手,也无助于缩短开发时间,因为当团队规模超过某个门槛时,再增加人手只会带来净损失。Fred Brooks在他的名著《人月神话》中描述了这种现象,我的所见所闻印证了他的说法。

  如果使用Lisp语言,程序能变得多短?以Lisp和C的比较为例,我听到的大多数说法是C代码的长度是Lisp的7倍到10倍。但是最近,New Architect杂志上有一篇介绍ITA软件公司的文章^,里面说“1行Lisp代码相当于20行C代码”,因为此文都是引用ITA总裁的话,所以我想这个数字来自ITA的编程实践。如果真是这样,那么我们可以相信这句话。ITA的软件不仅使用Lisp语言,还同时大量使用C和C++,所以这是他们的经验之谈。

^「Jen Muehlbauer,“Orbitz的新突破”(Orbitz Reaches New Heights),New Architect,2002年4月号。」

  我认为,这种比例肯定不会是一个常数。如果你遇到更困难的问题,或者你雇到了更聪明的程序员,这个比例就会增大。一种出色的工具到了真正优秀的黑客手里,可以发挥出更大的威力。

  总之,根据上面的这个数字,如果你与ITA竞争,而且你使用C语言开发软件,那么ITA的开发速度将比你快20倍。如果你需要一年时间实现某个功能,它只需要不到三星期。反过来说,如果ITA开发某个新功能用了三个月,那么你需要五年才能做出来。

  你知道吗?上面的对比还只是考虑到最好的情况。当我们只比较代码数量的时候,言下之意就是假设使用功能较弱的语言也能开发出同样的软件。但是事实上,程序员使用某种语言能做到的事情是有极限的。如果你想用一种低层次的语言解决一个很难的问题,那么你将会面临各种情况极其复杂乃至想不清楚的窘境。

  所以,当我说假定你与ITA竞争,你用五年时间做出的东西,ITA在Lisp语言的帮助下只用三个月就完成了,我指的五年还是一切顺利、没有犯错误、也没有遇到太大麻烦的五年。事实上,按照大多数公司的实际情况,计划中五年完成的项目很可能永远都不会完成。

  我承认,上面的例子太极端。ITA似乎有一批非常聪明的黑客,而C语言又是一种很低层次的语言。但是,在一个高度竞争的市场中,即使开发速度只相差两三倍,也足以使得你永远处在落后的位置。

一个诀窍

  由于选择了不当的编程语言而导致项目失败的可能性,是你的经理不愿意考虑的问题。事实上大部分的经理都这样。因为你知道,总的来说,你的经理其实不关心公司是否真的能获得成功,他真正关心的是不承担决策失败的责任。所以对他个人来说,最安全的做法就是跟随大多数人的选择。

  在大型组织内部,有一个专门的术语描述这种跟随大多数人的选择的做法,叫做“业界最佳实践”。这个词出现的原因其实就是为了让你的经理可以推卸责任。既然我选择的是“业界最佳实践”,如果不成功,项目失败了,那么你也无法指责我,因为做出选择的人不是我,而是整个“业界”。

  我认为这个词原来是指某种会计方法,大致意思就是不要采用很奇怪的处理方法。在会计方法中,这可能是一个很好的主意。“尖端”和“核算”这两个词听上去就不适合放在一起。但是如果你把这个标准引入技术决策,你就开始要出错了。

  技术本来就应该是尖端的。正如伊拉恩·加内特所说,编程语言的所谓“业界最佳实践”,实际上不会让你变成最佳,只会让你变得很平常。如果你选择的编程语言使得你开发软件的速度只有(选择更激进技术的)对手的几分之一,那么“最佳实践”真的起错了名字。

  所以,我们就有了两点结论,我认为它们非常有价值。事实上,这是我用自己的经历换来的。第一,不同语言的编程能力不一样。第二,大多数经理故意忽视第一点。你把这两点事实结合起来,其实就得到了赚钱的诀窍。ITA软件公司是运用这个诀窍的典型例子。如果你想在软件业获得成功,就使用你知道的最强大的语言,用它解决你知道的最难的问题,并且等待竞争对手的经理做出自甘平庸的选择。

附录:编程能力

  为了解释我所说的语言编程能力不一样,请考虑下面的问题。我们需要写一个函数,它能够生成累加器,即这个函数接受一个参数n,然后返回另一个函数,后者接受参数i,然后返回n增加(increment)了i后的值。[这里说的是增加,而不是n和i的相加(plus)。累加器就是应该完成n的累加。]

  Common Lisp^的写法如下:

(defun foo (n)
  (lambda (i) (incf n i)))

^「下面是一些Lisp方言生成累加器函数的写法:

Scheme:

(define (foo n)
  (lambda (i) (set! n (+ n i)) n))

Goo: (df foo (n) (op incf n _)))

Arc: (def foo (n) [++ n _])」

  Ruby的写法几乎完全相同:

def foo (n)
  lambda {|i| n += i } end

  Perl 5的写法则是:

sub foo {
  my ($n) = @_;
  sub {$n += shift}
}

  这比Lisp和Ruby的版本有更多的语法元素,因为在Perl语言中必须手工提取参数。

  Smalltalk的写法比Lisp和Ruby的稍微长一点:

foo: n
  |s|
  s := n.
  ^[:i| s := s+i. ]

  因为在Smalltalk中,词法变量(lexical variable)^是有效的,但是你无法给一个参数赋值,因此不得不设置了一个新变量,接受累加后的值。

^「词法变量,指的是变量的作用域由代码结构决定,不取决于运行时的调用顺序。也就是说,作用域在代码文本的词法分析阶段就决定了,而不在代码执行时决定。注意将这个概念与“局部变量”的概念相区分。——译者注」

  JavaScript的写法也比Lisp和Ruby稍微长一点,因为JavaScript依然区分语句和表达式,所以需要明确指定return语句来返回一个值:

function foo(n) {
  return function (i) {
    return n += i } }

  (实事求是地说,Perl也保留了语句和表达式的区别,但是使用了常规的Perl方式处理,因此可以省略return。)

  如果想把Lisp/Ruby/Perl/Smalltalk/JavaScript的版本改成Python,你会遇到一些限制。因为Pythcn并不完全支持词法变量,你不得不创造一种数据结构来接受n的值。而且尽管Python确实支持函数数据类型,但是没有一种字面量的表示方式(literal representation)可以生成函数(除非函数体只有一个表达式),所以你需要创造一个命名函数,把它返回。最后的写法如下:

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有一天会支持这样的写法。(如果不想等到Python慢慢进化到更像Lisp,总可以直接……)

  在面向对象编程的语言中,你能够在有限程度上模拟一个闭包(即一个函数,通过它可以引用由包含这个函数的代码所定义的变量)。你定义一个类(class),里面有一个方法和一个属性,用于替换封闭作用域(enclosing scope)中的所有变量。这有点类似于让程序员自己做代码分析,本来这应该是由支持词法作用域(lexical scope)的编译器完成的。如果有多个函数,同时指向相同的变量,那么这种方法就会失效,但是在这个简单的例子中,它已经足够了。

  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爱好者说我误解这种语言。但是在我看来,这两种写法好像都比第一个版本更复杂。你实际上就是在做同样的事,只不过划出了一个独立的区域保存累加器函数,区别只是保存在对象的一个属性中,而不是保存在列表 (list) 的头 (head) 中。使用这些特殊的内部属性名(尤其是call看上去并不像常规的解法,更像是一种破解。

  在Perl和Python的较量中,Python黑客的观点似乎是认为Python比Perl更优雅,但是这个例子表明,最终来说,编程能力决定了优雅程度。Perl的写法更简单(包含的语法元素更少),尽管它的语法有一点丑陋。

  其他语言怎么样?前文曾经提到过Fortran、C、C++、Java和Visual Basic,看上去使用它们根本无法解决这个问题。肯·安德森说,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;
    }};
}

  这种写法不符合题目要求,因为它只对整数有效。

  当然,我说使用其他语言无法解决这个问题,这句话并不完全正确。所有这些语言都是图灵等价的,这意味着严格地说,你能使用它们之中的任何一种语言写出任何一个程序。那么,怎样才能做到这一点呢?就这个小小的例子而言,你可以使用这些不那么强大的语言写一个Lisp解释器就行了。

  这样做听上去好像开玩笑,但是在大型编程项目中却不同程度地广泛存在。因此,有人把它总结出来,起名为“格林斯潘第十定律”(Greenspun's Tenth Rule):

任何C或Fortran程序复杂到一定程度之后,都会包含一个临时开发的、只有一半功能的、不完全符合规格的、到处都是bug的、运行速度很慢的Common Lisp实现。

  如果你想解决一个困难的问题,关键不是你使用的语言是否强大,而是好几个因素同时发挥作用:(a)使用一种强大的语言;(b)为这个难题写一个事实上的解释器;或者(c)你自己变成这个难题的人肉编译器。在Python的例子中,这样的处理方法已经开始出现了,我们实际上就是自己写代码,模拟出编译器实现词法变量的功能。

  这种实践不仅很普遍,而且已经制度化了。举例来说,在面向对象编程的世界中,我们大量听到“模式”(pattern)这个词,我觉得那些“模式”就是现实中的因素(c),也就是人肉编译器^。当我在自己的程序中发现用到了模式,我觉得这就表明某个地方出错了。程序的形式应该仅仅反映它所要解决的问题。代码中其他任何外加的形式都是一个信号,(至少对我来说)表明我对问题的抽象还不够深,也经常提醒我,自己正在手工完成的事情,本应该写代码通过宏的扩展自动实现。

「皮特·诺维格发现,总共23种设计模式之中,有16种在Lisp语言中“本身就提供,或者被大大简化”。(www.norvig.com/design-pattems)」

原文链接: https://www.kancloud.cn/imxieke/hacker-and-painter/107332

转载请注明:我的生活记忆 » 黑客与画家 13.书呆子的复仇

喜欢 (1)or分享 (0)