PG Paul Graham 文集
100%
Part 5 · 早期文章 Early Essays

备受欢迎

2001年5月

(本文是作为一种新语言的商业计划书而写的。因此它遗漏了(因为视为理所当然)一个好编程语言最重要的特性:非常强大的抽象能力。)

我的朋友曾告诉一位杰出的操作系统专家,他想设计一种真正好的编程语言。专家告诉他这是浪费时间,编程语言的流行与否并不取决于其优劣,所以无论他的语言有多好,也不会有人使用。至少,设计的语言就是如此。

那么,是什么让一种语言流行起来呢?流行的语言是否配得上它们的流行度?值得尝试去定义一种好的编程语言吗?你又该怎么做?

我认为,这些问题的答案可以通过观察黑客,了解他们的需求来找到。编程语言是黑客而生的,一种编程语言作为编程语言(而非指称语义学或编译器设计的练习)是好的,当且仅当黑客喜欢它。

1 流行的机制

诚然,大多数人并非单纯根据语言的优劣来选择编程语言。大多数程序员是由别人告诉他们使用什么语言的。然而,我认为这些外部因素对编程语言流行度的影响,并不像有时人们想象的那么大。我认为更大的问题在于,黑客心目中好的编程语言,与大多数语言设计师的想法并不相同。

在这两者之间,黑客的意见才是关键。编程语言不是定理。它们是工具,是为人类设计的,必须像鞋子要为人类的脚设计一样,适合人类的优点和弱点。如果一双鞋穿上就夹脚,那就是一双坏鞋,无论它作为一件雕塑品有多么优雅。

也许大多数程序员无法分辨语言的好坏。但这与其他任何工具并无不同。这并不意味着尝试设计一种好的语言就是浪费时间。专家级黑客一眼就能看出一种语言的好坏,他们会使用它。诚然,专家级黑客是极少数,但这极少数人编写了所有优秀的软件,而且他们的影响力使得其他程序员倾向于使用他们所用的任何语言。事实上,这往往不仅仅是影响力,而是指令:常常是这些专家级黑客,作为老板或导师,告诉其他程序员使用什么语言。

专家级黑客的意见并非决定编程语言相对流行度的唯一力量——遗留软件(Cobol)和炒作(Ada、Java)也起作用——但我认为从长远来看,它是最强大的力量。有了最初的临界规模和足够的时间,一种编程语言可能会变得与其应得的流行度相当。而流行度进一步将好语言与坏语言区分开来,因为来自真实用户的反馈总会带来改进。看看任何一种流行语言在其生命周期中发生了多大的变化。Perl和Fortran是极端案例,但即使是Lisp也改变了很多。例如,Lisp 1.5没有宏;这些是后来才演化出来的,是在MIT的黑客花费数年时间用Lisp编写真实程序之后。[1]

因此,无论一种语言是否必须优秀才能流行,我认为一种语言必须流行才能变得优秀。而且它必须保持流行才能保持优秀。编程语言的技术水平不会停滞不前。然而我们今天拥有的Lisp,仍然与上世纪80年代中期MIT的Lisp差不多,因为那是Lisp最后一次拥有足够庞大和苛刻的用户群。

当然,黑客必须知道一种语言才能使用它。他们怎么听说呢?从其他黑客那里。但必须有一个最初的黑客群体使用这种语言,其他人才能听说它。我想知道这个群体需要多大;多少用户构成临界规模?凭直觉,我会说二十个。如果一种语言有二十个独立的用户,意思是二十个自己决定使用它的用户,我就会认为它是真实的。

达到这个目标绝非易事。如果从零到二十比从二十到一千更难,我也不会感到惊讶。获得最初二十个用户的最佳方式可能是使用特洛伊木马:给人们一个他们想要的应用,而这个应用恰好是用新语言编写的。

2 外部因素

让我们首先承认一个确实影响编程语言流行度的外部因素。要变得流行,一种编程语言必须成为一种流行系统的脚本语言。Fortran和Cobol是早期IBM大型机的脚本语言。C是Unix的脚本语言,后来Perl也是。Tcl是Tk的脚本语言。Java和Javascript意在成为网页浏览器的脚本语言。

Lisp不是一种大规模流行的语言,因为它不是大规模流行系统的脚本语言。它保留的流行度可以追溯到1960年代和1970年代,那时它是MIT的脚本语言。当时许多伟大的程序员都在某个时候与MIT有关联。而在1970年代早期,在C语言出现之前,MIT的Lisp方言(称为MacLisp)是认真的黑客想要使用的少数编程语言之一。

如今,Lisp是两种中等流行系统(Emacs和Autocad)的脚本语言,因此我怀疑今天大部分Lisp编程是用Emacs Lisp或AutoLisp完成的。

编程语言并非孤立存在。黑客是一个及物动词——黑客通常是在黑客某样东西——而在实践中,语言是相对于它们被用来黑客的对象来评判的。所以,如果你想设计一种流行的语言,你要么提供比语言本身更多的东西,要么你必须设计你的语言来取代某个现有系统的脚本语言。

Common Lisp不流行的部分原因是它是一个孤儿。它最初确实带有一个可以黑客的系统:Lisp Machine。但Lisp Machine(连同并行计算机)在1980年代被越来越强大的通用处理器碾压了。如果Common Lisp是Unix的一个好的脚本语言,它可能仍然保持流行。可惜,它是一个糟糕透顶的脚本语言。

描述这种情况的一种方式是说,语言不是凭自身优劣来评判的。另一种观点是,一种编程语言除非同时也是某样东西的脚本语言,否则它真的不算是一种编程语言。这只有在令人意外时才会显得不公平。我认为这就像期望一种编程语言有一个实现一样公平。它只是编程语言定义的一部分。

当然,一种编程语言确实需要一个好的实现,而且必须是免费的。公司会为软件付费,但个体黑客不会,而你需要吸引的正是黑客。

一种语言还需要有一本关于它的书。这本书应该薄、写得很好、充满好例子。K&R是这里的典范。目前我几乎可以说,一种语言必须有一本由O’Reilly出版的书。这正在成为衡量它对黑客是否重要的标准。

也应该有在线文档。事实上,这本书可以从在线文档开始。但我认为实体书还没有过时。它们的格式很方便,出版商施加的事实上的审查是一个有用尽管不完美的过滤器。书店是了解新语言最重要的场所之一。

3 简洁

假设你能提供任何语言都需要的三样东西——免费实现、一本书和可黑客的对象——你如何制造一种黑客会喜欢的语言?

黑客喜欢的一样东西是简洁。黑客是懒惰的,就像数学家和现代主义建筑师那样懒惰:他们讨厌任何多余的东西。如果说一个即将编写程序的黑客决定使用什么语言,至少下意识地,是基于他必须键入的字符总数,这离事实不远。即使这不是黑客的确切思考方式,语言设计师也应该假装是这样来行事。

试图用冗长的、模仿英语的表达式来迁就用户是错误的。Cobol因这一缺陷而臭名昭著。一个黑客会把被要求写

add x to y giving z

而不是

z = x+y

视为既是对他智力的侮辱,又是对上帝的亵渎。

有时有人说Lisp应该用first和rest而不是car和cdr,因为这会让程序更容易阅读。也许在最初的几个小时是这样。但一个黑客能很快学会car意味着列表的第一个元素,cdr意味着其余部分。使用first和rest意味着多打50%的字。而且它们的长度也不同,意味着当它们像car和cdr经常做的那样在连续行中被调用时,参数不会对齐。我发现代码在页面上的对齐非常重要。当Lisp代码用变宽字体排版时,我几乎无法阅读,朋友们说其他语言也是如此。

简洁是强类型语言吃亏的地方之一。在所有其他条件相同的情况下,没有人想在程序开头写一堆声明。任何可以隐含的东西,都应该隐含。

单独的记号也应该简短。Perl和Common Lisp在这个问题上处于两个极端。Perl程序几乎可以像密码一样密集,而Common Lisp内置操作符的名字则长得可笑。Common Lisp的设计者可能期望用户有能替他们键入这些长名字的文本编辑器。但长名字的代价不仅仅是敲击它的代价。还有阅读它的代价,以及它在屏幕上占据空间的代价。

4 可黑客性

对黑客来说,有一件事比简洁更重要:能够做你想做的事。在编程语言的历史上,令人惊讶的大量努力被用来阻止程序员做被认为是不当的事情。这是一个危险的、自以为是的计划。语言设计师怎么知道程序员需要做什么?我认为语言设计师最好把他们的目标用户视为一个天才,他将需要做他们从未预料到的事情,而不是一个需要被保护以免伤害自己的笨蛋。笨蛋无论如何都会搬起石头砸自己的脚。你也许能阻止他引用另一个包中的变量,但你无法阻止他编写一个设计糟糕的程序来解决错误的问题,并且花费无尽的时间。

优秀的程序员常常想做危险和不讨好的事情。所谓不讨好,我指的是那些绕过语言试图呈现的语义表象的事情:例如,获取某个高级抽象的内部表示。黑客喜欢黑客,而黑客意味着深入事物内部,对原始设计师的决定进行二次猜测。

让自己被二次猜测。当你制造任何工具时,人们会以你未预料的方式使用它,对于像编程语言这样高度精细的工具尤其如此。许多黑客会想以你从未想象过的方式调整你的语义模型。我说,让他们来吧;在不危及像垃圾收集器这样的运行时系统的前提下,尽可能多地让程序员接触内部机制。

在 Common Lisp 中,我常常希望能遍历一个结构体(struct)的各个字段——例如,梳理出对已删除对象的引用,或找出未初始化的字段。我知道结构体底层不过是向量而已。然而,我却无法编写一个通用的函数,将其应用于任何结构体。我只能通过字段名来访问它们,因为这就是结构体的本义。

黑客或许只是想在大型程序中一两次地颠覆事物的既定模型。但能做到这一点,带来的差别何其之大。而且这或许不仅仅是解决问题本身。其中也有一种乐趣。黑客们共享着外科医生翻弄粗粝内脏时那种隐秘的快感,也共享着少年挤痘痘时那种隐秘的快感。[2] 至少对男孩而言,某些恐怖之事颇具吸引力。《Maxim》杂志每年出版一册摄影集,里面混杂着美女图片和惨烈事故的照片。他们深知自己的读者群。

从历史上看,Lisp 一直擅长让黑客为所欲为。Common Lisp 的政治正确是一种反常现象。早期的 Lisp 让你能触及一切。幸运的是,那种精神的很大一部分被保留在了宏(macro)中。能够在源代码上执行任意变换,这是多么美妙的事情。

经典宏是真正的黑客工具——简单、强大,且危险。理解它们的作用极其容易:你对宏的参数调用一个函数,其返回结果就被插入到宏调用的位置。卫生宏(hygienic macro)则体现了相反的原则。它们试图保护你,不让你理解它们在做什么。我从未听过有人能用一句话解释清楚卫生宏。而它们正是“替程序员决定他们可以想要什么”这种做法的危险性的经典例子。卫生宏意在保护我免受变量捕获(variable capture)等问题的困扰,但在某些宏中,变量捕获恰恰是我想要的。

一门真正优秀的语言应当既干净又肮脏:设计干净,拥有一个规模小、理解透彻且高度正交的算子核心;但在“让黑客随心所欲”这个意义上又是肮脏的。C 语言就是这样。早期的 Lisp 也是如此。真正属于黑客的语言总是带有一丝轻佻的气质。

一门好的编程语言应该具备一些特性,让那些常用“软件工程”这类词汇的人不以为然地摇头。而在这个连续谱的另一端,则是 Ada 和 Pascal 这样的语言,它们是规整的典范,只适合教学,别的用处不大。

5 一次性程序

要吸引黑客,一种语言必须擅长编写他们想写的那类程序。这意味着——也许出人意料——它必须擅长编写一次性程序(throwaway program)。

一次性程序是指你为某个有限任务快速编写的程序:比如自动化某个系统管理任务、为仿真生成测试数据,或将数据从一种格式转换为另一种格式。关于一次性程序,令人惊讶的一点是,它们就像二战期间美国许多大学搭建的“临时”建筑一样,往往并不会被丢弃。许多会演变成真正的程序,拥有真正的功能和真正的用户。

我有个直觉:最优秀的大型程序都是以这种方式开始生命的,而不是从一开始就像胡佛水坝那样被设计得庞大。从零开始构建一个庞大的东西是可怕的。当人们接手一个过大的项目时,他们会不堪重负。项目要么陷入泥潭,要么结果枯燥呆板:那是一个购物中心而非真正的市中心,是巴西利亚而非罗马,是 Ada 而非 C。

获取大型程序的另一种方式,是从一个一次性程序开始,并不断改进它。这种方法不那么令人生畏,而且程序的设计会从演化中获益。我想,如果去考察的话,会发现这其实是大多数大型程序的开发方式。而那些确实以这种方式演化而来的程序,很可能仍在使用最初编写它们所用的语言,因为程序被移植的情况很少见,除非出于政治原因。于是,矛盾的是,如果你想制造一种用于大型系统的语言,你就必须让它擅长编写一次性程序,因为大型系统正来源于此。

Perl 是这一思想的绝佳例证。它不仅是为了编写一次性程序而设计的,其本身在很大程度上就是一个一次性程序。Perl 最初只是用于生成报告的一组工具,随着人们用它编写的一次性程序不断长大,它才逐渐演变成一种编程语言。直到 Perl 5(如果那时算的话),它才适合编写严肃程序,但在此之前它已经极为流行了。

什么使一种语言适合编写一次性程序?首先,它必须易于获得。一次性程序是你期望在一小时内写完的东西。因此,这种语言很可能必须已经安装在你正在使用的计算机上。它不能是你需要先安装才能使用的东西。它必须就在那里。C 语言就在那里,因为它随操作系统一同提供。Perl 也在那里,因为它最初是系统管理员的工具,而且你的系统管理员已经安装好了它。

“可用”不仅仅是“已安装”的意思,虽然这也很重要。一种具有命令行界面的交互式语言,比那种需要编译然后单独运行的语言更“可用”。一种流行的编程语言应该是交互式的,并且启动要快。

编写一次性程序时你想要的另一件事是简洁。简洁对黑客始终有吸引力,而在一小时内就要完成的程序中,这一点尤为突出。

6 库

当然,简洁的极致是程序已经为你写好了,你只需调用它而已。这就引出了我认为未来编程语言中会越来越重要的一个特性:库函数。Perl 之所以胜出,是因为它拥有庞大的字符串处理库。这类库函数对于一次性程序尤其重要,因为这类程序最初常常是为了转换或提取数据而写的。许多 Perl 程序可能一开始只是把几个库调用拼在一起。

我认为未来五十年编程语言的许多进步将与库函数有关。我认为未来的编程语言将拥有像核心语言一样精心设计的库。编程语言设计将不再纠结于让你的语言是强类型还是弱类型、面向对象还是函数式,或者其他什么,而是关乎如何设计伟大的库。那些喜欢思考如何设计类型系统的语言设计师们可能会对此不寒而栗。这几乎就像在编写应用程序!可惜了。语言是为程序员服务的,而库正是程序员所需。

设计好的库是困难的。这不仅仅是写大量代码的问题。一旦库变得太大,有时找到你需要的函数所花的时间,比你自己写代码还要长。库需要使用一小组正交算子来设计,就像核心语言一样。程序员应当能够猜到哪个库调用能实现他的需求。

库是 Common Lisp 的一个不足之处。它只有非常基础的字符串操作库,而且几乎没有与操作系统通信的库。由于历史原因,Common Lisp 试图假装操作系统不存在。而因为你无法与操作系统对话,你就很难仅用 Common Lisp 的内置算子来编写严肃的程序。你还必须使用一些特定实现的技巧,而在实践中,这些往往不能完全满足你的需求。如果 Common Lisp 拥有强大的字符串库和良好的操作系统支持,黑客们对 Lisp 的评价会高得多。

7 语法

一门具有 Lisp 那样语法——更准确地说,缺乏语法——的语言,能否变得流行?我不知道这个问题的答案。我确实认为,语法并非 Lisp 当前不流行的主要原因。Common Lisp 有比不熟悉的语法更严重的问题。我认识几位对前缀语法很习惯的程序员,却默认使用 Perl,因为它有强大的字符串库并能与操作系统通信。

前缀记号有两个可能的问题:一是对程序员来说不熟悉,二是密度不够。Lisp 世界的主流观点认为第一个问题是真正的问题。我不太确定。是的,前缀记号会让普通程序员恐慌。但我认为普通程序员的意见不重要。语言的流行与否取决于专家黑客的看法,而我认为专家黑客或许能够应对前缀记号。Perl 的语法可能相当晦涩难懂,但这并未妨碍 Perl 的流行。如果要说有什么影响,那可能反而促成了 Perl 的崇拜文化。

更严重的问题是前缀记号的松散性。对专家黑客来说,这确实是个问题。当能写 a[x,y] 时,没人想写 (aref a x y)。

在这个具体情况下,有一种方法可以巧妙地规避这个问题。如果我们把数据结构当作对索引的函数,我们就可以写 (a x y),这甚至比 Perl 的形式更短。类似的技巧或许也能缩短其他类型的表达式。

我们可以通过让缩进具有意义来去掉(或使其可选)很多括号。反正程序员读代码时就是这样:当缩进说一件事情,而分隔符说另一件事情时,我们按缩进来。将缩进视为有意义,不仅能消除这一常见的 bug 来源,还能让程序更短。

有时中缀语法更容易阅读。数学表达式尤其如此。我在整个编程生涯中都使用 Lisp,但我仍然觉得中缀数学表达式不自然。然而,拥有可接受任意数量参数的算子是很方便的,尤其是在生成代码时。所以,如果我们确实引入中缀语法,或许应该以某种读取宏(read-macro)的方式实现。

我认为我们不应在宗教层面上反对向 Lisp 引入语法,只要它能以被充分理解的方式翻译为底层的 s-表达式。Lisp 中已经有不少语法了。引入更多语法未必是坏事,只要没有人被迫使用它。在 Common Lisp 中,一些分隔符被保留给语言本身,这暗示至少部分设计者打算在未来增加更多语法。

Common Lisp 中最不像 Lisp 的语法片段之一出现在 format 字符串中;format 本身是一门语言,而那门语言不是 Lisp。如果有一个向 Lisp 引入更多语法的计划,format 说明符或许可以被包含其中。如果宏能像生成其他任何代码一样生成 format 说明符,那将是件好事。

一位杰出的 Lisp 黑客告诉我,他的那本 CLTL 总是会自动翻开到 format 那一节。我的也是。这可能表明还有改进余地。这也可能意味着程序做了大量的 I/O 操作。

8 效率

众所周知,一门好语言应该生成快速的代码。但我认为在实践中,快速代码并非主要来源于语言设计中的那些做法。正如 Knuth 很久以前指出的,速度只在某些关键瓶颈处才重要。而且正如许多程序员后来观察到的,人们常常错误判断这些瓶颈在哪里。

因此,在实践中,获得快速代码的方式是拥有一个非常好的性能分析器(profiler),而不是例如让语言变成强类型。你不需要知道程序中每个调用的每个参数的类型。你需要能够在瓶颈处声明参数类型。更重要的是,你需要能够找出瓶颈在哪里。

人们对 Lisp 的一个抱怨是,很难分辨哪些操作代价高昂。这或许属实,也可能是在追求高度抽象语言时不可避免的结果。无论如何,我认为一个好的性能分析器(profiler)能在很大程度上解决这个问题:你很快就能学会分辨什么是昂贵的。

这里部分问题在于社会层面。语言设计者热衷于编写快速的编译器,他们以此衡量自己的技能。他们至多将性能分析器视为一个附加组件。但在实践中,一个好的性能分析器对于提升用该语言编写的实际程序运行速度,可能比生成快速代码的编译器贡献更大。在此,语言设计者再次与用户有些脱节——他们确实出色地解决了一个略有偏差的问题。

采用主动式性能分析器(active profiler)或许是个好主意——主动将性能数据推送给程序员,而非等待他们来查询。例如,当程序员编辑源代码时,编辑器可以用红色标注瓶颈。另一种方法是以某种方式可视化运行中程序的实时状态。这在基于服务器的应用中尤其能带来巨大好处,因为那里有大量运行中的程序可供观察。一个主动分析器可以图形化展示程序运行时的内存状况,甚至通过声音来提示当前发生的情况。

声音是发现问题的良好线索。我曾工作过的一个地方,有一块布满仪表的大面板,显示着我们的网络服务器状态。指针由小型伺服电机驱动,转动时会发出轻微声响。虽然从我的办公桌看不到面板,但我发现仅凭声音就能立刻判断出服务器是否出了问题。

甚至有可能编写一个能自动检测低效算法的性能分析器。如果某些内存访问模式被证实是糟糕算法的确切标志,我也不会感到惊讶。如果有一个小人在计算机内部执行我们的程序,他关于自己工作的叙述,或许会和联邦政府雇员一样冗长而哀怨。我常常感觉自己在让处理器四处奔波徒劳,却从未有过好的方法来观察它到底在做什么。

目前许多 Lisp 实现会编译成字节码,再由解释器执行。这样做通常是为了便于移植实现,但这本身可能是一个有用的语言特性。将字节码正式纳入语言规范,并允许程序员在瓶颈处使用内联字节码,或许是个好主意。这样,此类优化也将具备可移植性。

从终端用户的角度来看,速度的本质可能正在改变。随着基于服务器的应用兴起,越来越多的程序可能会变成 I/O 密集型。让 I/O 变快将非常有价值。语言可以通过诸如简单、快速、格式化的输出函数等直接手段,以及缓存和持久化对象等深层次的结构性变革来提供帮助。

用户关心响应时间。但另一种效率将变得越来越重要:每个处理器能支持的并发用户数。未来许多有趣的应用都将是基于服务器的,而对任何托管此类应用的人来说,每台服务器能服务的用户数量是关键问题。对于提供服务器应用的商业公司而言,这直接关系到资本成本的分母。

多年来,在大多数终端用户应用中,效率并非至关重要。开发者一直可以假设每位用户桌上都有一台性能不断提升的处理器。根据帕金森定律,软件会膨胀以占满可用资源。这种情况将随着服务器应用的普及而改变。在那个世界里,硬件和软件将一同提供。对于提供服务器应用的公司来说,每台服务器能支持多少用户,将对利润产生重大影响。

在某些应用中,处理器将成为限制因素,执行速度将是最需要优化的。但通常内存会成为瓶颈;并发用户数取决于每个用户数据所需的内存大小。语言在这方面也能提供帮助。对线程的良好支持将使所有用户共享同一个堆。拥有持久化对象和/或语言级别的懒加载支持也可能有所帮助。

9 时间

一个流行语言所需的最后一个要素是时间。没有人愿意用一门可能会消失的语言编写程序,而许多编程语言正是如此。因此,大多数黑客会倾向于等一门语言出现几年后,才考虑使用它。

新奇事物的发明者常常惊讶地发现这一点,但你需要时间才能让信息传达给人们。我有一个朋友,很少在别人第一次请求时就答应。他知道人们有时会提出一些他们最终发现自己并不真正想要的东西。为了避免浪费时间,他会等到第三或第四次被请求时才去做;到那时,请求者可能已经相当恼火,但至少他们可能确实想要他们所请求的东西。

大多数人已经学会了对他们听到的新事物进行类似的筛选。他们直到听到某件事十次之后才开始关注。他们完全有理由这样做:大多数所谓的热门新事物最终确实被证明是浪费时间,并最终消失。因为推迟学习 VRML,我得以完全不用学它。

因此,任何发明新事物的人都必须预期,要持续多年地重复自己的信息,人们才会开始理解。我们编写了据我所知是第一个基于 Web 服务器的应用程序,但我们花了好几年才让人们明白它无需下载。这并不是因为他们愚蠢,只是他们把我们“屏蔽”了。

好消息是,简单的重复就能解决问题。你所要做的就是不断讲述你的故事,最终人们会开始听到。人们注意你,不是在你出现时,而是在他们注意到你依然存在时。

通常需要一段时间才能获得势头,这也许是件好事。大多数技术即使在首次推出后也会经历大量演变——编程语言尤其如此。对于一个新技术来说,没有什么比被少数早期采用者使用几年更好的了。早期采用者精明且要求苛刻,能迅速清除你技术中残留的任何缺陷。当你只有少数用户时,你可以与他们所有人保持密切联系。而且,当你改进系统时,即使引起一些破坏,早期采用者也会很宽容。

新技术的引入有两种方式:有机增长法和“大爆炸”法。有机增长法的典型代表是经典的、凭感觉、资金不足的车库创业。一两个人默默无闻地开发新技术,在没有任何营销的情况下推出,最初只有少数(狂热的)用户。他们持续改进技术,同时用户群通过口口相传不断增长。不知不觉中,他们就成了大公司。

另一种方法,“大爆炸”法,则以风投支持、市场推广强劲的初创公司为代表。他们匆忙开发产品,高调发布,并希望立即(他们希望)拥有庞大的用户群。

通常,车库创业者羡慕“大爆炸”创业者。“大爆炸”创业者圆滑、自信,深受风投资本家尊重。他们能负担得起最好的一切,围绕发布的公关活动还附带让他们成为名人的效果。而有机增长者坐在车库里,感到贫穷和不被关爱。然而,我认为他们常常误以为自己值得同情。有机增长似乎比“大爆炸”方法能产生更好的技术和更富有的创始人。如果你看看当今的主导技术,你会发现它们大多是有机增长起来的。

这种模式不仅适用于公司。在赞助研究中也能看到。Multics 和 Common Lisp 是大爆炸项目,而 Unix 和 MacLisp 则是有机增长项目。

10 重新设计

“最好的写作是重写,”E. B. 怀特写道。每个优秀的作家都知道这一点,软件也是如此。设计中最重要的部分是重新设计。编程语言尤其没有得到足够的重新设计。

要写出好的软件,你必须同时在脑海中保持两个对立的观念。你需要年轻黑客对自己能力的天真信心,同时也要有老手的怀疑精神。你必须能用一半的大脑想“这能有多难?”,同时用另一半想“这永远行不通”。

诀窍在于意识到这里并没有真正的矛盾。你要对两件不同的事情保持乐观和怀疑。你必须对解决问题的可能性保持乐观,但对目前所得解决方案的价值持怀疑态度。

做出优秀工作的人常常认为他们正在做的事情不好。别人看到他们已完成的工作会充满惊叹,但创造者却满心忧虑。这种模式并非巧合:正是这种忧虑让工作变得出色。

如果你能让希望和忧虑保持平衡,它们会像你双腿驱动自行车前进一样推动项目前进。在两阶段创新引擎的第一阶段,你受着能够解决问题的信心鼓舞,拼命工作。在第二阶段,你在清晨的冷光下审视自己已完成的工作,清晰地看到所有缺陷。但只要你批判的精神没有压过希望,你就能看着自己那公认不完善的系统,心想“剩下的路能有多难?”从而继续这个循环。

保持两种力量平衡很棘手。年轻黑客中,乐观占主导。他们做出一些东西,确信它很棒,就再也不改进了。在老黑客那里,怀疑占上风,他们甚至不敢承担雄心勃勃的项目。

任何能让重新设计循环继续下去的事情都是好的。散文可以一遍又一遍地重写,直到你满意为止。但软件,作为规则,没有得到足够的重新设计。散文有读者,但软件有用户。如果作家重写一篇文章,读过旧版本的人不太可能抱怨他们的想法被新引入的不兼容性破坏了。

用户是一把双刃剑。他们可以帮助你改进你的语言,但也会阻碍你改进它。所以要谨慎选择你的用户,并缓慢增加他们的数量。拥有用户就像优化一样:明智的做法是推迟它。此外,作为一般规则,你在任何特定时间都能比你想的做出更多改变而不受惩罚。引入改变就像撕绷带:疼痛几乎在感觉到的瞬间就变成了记忆。

众所周知,由委员会设计语言不是一个好主意。委员会产生糟糕的设计。但我认为委员会最大的危险在于他们干扰了重新设计。引入变更工作量巨大,以至于没人愿意费心。委员会无论决定什么,往往会保持不变,即使大多数成员不喜欢它。

即使是两人的委员会也会妨碍重新设计。这在由两个不同人编写的软件之间的接口处尤其如此。要改变接口,双方必须同时同意改变。因此接口往往根本不会改变,这很成问题,因为它们往往是任何系统中临时拼凑(ad hoc)最严重的部分之一。

一种可能的解决方案是设计系统,使接口水平化而非垂直化——这样模块始终作为抽象的垂直层次堆叠。这样,接口往往由其中一个模块掌控。两层中较低的一层,要么是编写上层所使用的语言,此时接口由下层掌控;要么它是从属层,此时接口可由上层规定。

11 Lisp

这一切意味着,新的Lisp仍有希望。任何能为黑客提供他们所需之物的语言都有希望,Lisp也不例外。我想我们可能犯了个错误,以为黑客们是被Lisp的怪异之处吓跑的。这种令人安慰的错觉可能阻碍了我们看清Lisp真正的问题,至少是Common Lisp的问题——它在满足黑客需求方面表现欠佳。黑客的语言需要强大的库和可玩的东西。Common Lisp两者皆无。黑客的语言应当简洁、易于破解。Common Lisp并非如此。

好消息是,问题不在于Lisp本身,而在于Common Lisp。如果我们能开发出一种真正属于黑客的新Lisp,我想黑客们会乐于采用。他们会使用任何能解决问题的语言。我们只需确保这个新Lisp在某些重要任务上比其他语言做得更好。

历史给了我们一些启示。随着时间的推移,新一代编程语言不断从Lisp中汲取越来越多的特性。在语言最终演变成Lisp之前,可复制的东西已所剩不多。当前最热门的语言Python,不过是Lisp的简化版,采用中缀语法且无宏。一个新的Lisp将是这一进程中的自然一步。

我有时会想,将其称为Python的改进版或许是个不错的营销策略,那听起来比Lisp更时髦。对许多人而言,Lisp是一门带有大量括号的缓慢的AI语言。Fritz Kunze的官方传记小心翼翼地避开了L开头的词。但我猜我们不必害怕将新Lisp称为Lisp。Lisp在最顶尖的黑客中仍享有潜在的尊重——例如那些学过6.001课程并真正理解其精髓的人。而这些正是你需要争取的用户。

在《如何成为一名黑客》中,埃里克·雷蒙德将Lisp比作拉丁语或希腊语——一种即便实际上不会使用,也应作为智力锻炼去学习的语言:

Lisp值得学习,因为当你最终领悟它时,会获得深刻的启迪体验;这种体验会让你在余生的编程生涯中成为更好的程序员,哪怕你实际上并不经常使用Lisp本身。

如果我不了解Lisp,读到这段话会引发我诸多疑问。一种能让我成为更好程序员的语言,如果真的有意义的话,就意味着一种更适合编程的语言。而这正是埃里克所言所隐含的意思。

只要这个想法还在流传,我想黑客们对新的Lisp仍会持开放态度,即便它被称为Lisp。但这个Lisp必须是黑客的语言,就像20世纪70年代的经典Lisp那样。它必须简洁、简单、易于破解。而且它必须具备强大的库,以满足黑客当下的需求。

在库的方面,我认为有空间在Perl和Python擅长的领域击败它们。未来几年需要编写的许多新应用将是基于服务器的应用。没有理由说新的Lisp不能拥有与Perl同样优秀的字符串库,而如果这个新Lisp还拥有强大的服务器应用库,它可能会非常受欢迎。真正的黑客不会对一款能让他们通过几次库调用解决难题的新工具嗤之以鼻。记住,黑客是懒惰的。

在核心语言层面支持基于服务器的应用,可能带来更大的胜利。例如,对多用户程序的显式支持,或在类型标签层面的数据所有权。

基于服务器的应用也为我们回答了新Lisp将用来破解什么的问题。将Lisp改进为Unix的脚本语言并无坏处(再差也差不到哪去)。但我认为存在一些现有语言更容易被击败的领域。或许更好的做法是效仿Tcl的模式,将Lisp与一套完整的服务器应用支持系统一起提供。Lisp天然适合服务器应用。词法闭包提供了一种在用户界面仅为一系列网页时实现子程序效果的方式。S-表达式能很好地映射到HTML,而宏则擅长生成HTML。我们需要更好的工具来编写服务器应用,也需要一个新的Lisp,这两者将结合得非常好。

12 梦想语言

总结一下,让我们尝试描述黑客的梦想语言。梦想语言是美观、干净、简洁的。它有一个启动迅速的交互式顶层。你只需极少的代码就能编写程序解决常见问题。你所写的任何程序中,几乎全部代码都是针对具体应用的。其他一切均已为你准备好。

语言的语法简洁到了极致。你永远不必输入多余的字符,甚至不必频繁使用Shift键。

利用大型抽象,你可以非常迅速地编写程序的第一版。之后,当你想要优化时,有一个非常好的性能分析器告诉你应该关注哪里。你可以让内层循环飞快运行,甚至可以在需要时编写内联字节码。

有大量优秀的示例可供学习,而且语言足够直观,你可以在几分钟内通过示例学会如何使用。你不需要经常查阅手册。手册很薄,且少有警告和限制条件。

语言有一个小型核心,以及强大且高度正交的库,这些库的设计与核心语言一样精心。这些库都能很好地协同工作;语言中的一切就像精密相机中的零件一样契合。没有什么是过时的,或为了兼容而保留的。所有库的源代码都易于获取。与操作系统以及与用其他语言编写的应用程序交互都很容易。

语言是分层构建的。更高级的抽象以非常透明的方式构建在较低级别的抽象之上,如果你愿意,你可以掌握这些较低级别的抽象。

没有什么是必须对你隐瞒的。语言提供抽象只是为了节省你的工作,而不是告诉你去做什么。事实上,语言鼓励你平等地参与其设计。你可以改变它的一切,甚至包括语法,并且你所写的任何东西,都尽可能与预定义内容具有同等地位。

注释

[1] 非常接近现代观念的宏由Timothy Hart于1964年提出,也就是Lisp 1.5发布两年后。最初缺失的是避免变量捕获和多重评估的方法;Hart的示例存在这两方面的问题。

[2] 在《当空气击中你的大脑》中,神经外科医生Frank Vertosick回忆了一段对话,他的住院总医师Gary谈到了外科医生与内科医生(“跳蚤”)之间的区别:

Gary和我点了一个大披萨,找了个空卡座坐下。住院总医师点了一根烟。“看看那些该死的跳蚤,叽叽喳喳地谈论着他们一辈子可能也遇不上一次的疾病。这就是跳蚤的问题,他们只喜欢古怪的东西。他们讨厌自己的家常病例。这就是我们和那些该死的跳蚤的区别。看,我们喜欢多汁的大块腰椎间盘突出,但他们讨厌高血压……”

很难把腰椎间盘突出想象成“多汁的”(除非从字面意义上)。然而我想我明白他们的意思。我经常遇到一个“多汁”的bug要追踪。非程序员很难想象bug中竟能找到乐趣。当然,一切正常运转肯定更好。从某方面说,确实如此。但不可否认,追踪某些类型的bug时,有一种冷酷的满足感。