流行之道

2001年5月

(本文是为一种新语言撰写的一种商业计划。因此,它缺少(因为它理所当然地认为)一个好的编程语言最重要的特性:非常强大的抽象能力。)

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

那究竟是什么让一种语言流行起来呢?流行的语言是否名副其实?尝试定义一种好的编程语言值得吗?你会怎么做?

我认为这些问题的答案可以通过观察黑客并了解他们想要什么来找到。编程语言是_为_黑客而生的,如果且仅当黑客喜欢它时,一种编程语言才算得上是一种好的编程语言(而不是,比如说,指称语义学或编译器设计的练习)。

1 流行机制

当然,大多数人选择编程语言并非仅仅基于其优点。大多数程序员都是由他人告知使用何种语言。然而,我认为这些外部因素对编程语言流行的影响并不像有时人们认为的那么大。我认为一个更大的问题是,黑客对好的编程语言的看法与大多数语言设计者的看法不同。

两者之间,黑客的意见才是最重要的。编程语言不是定理。它们是工具,为人而设计,它们必须像鞋子必须为人的脚设计一样,既要适应人类的优点,也要适应人类的缺点。如果一双鞋穿上会夹脚,那它就是一双坏鞋,无论它作为一件雕塑多么优雅。

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

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

所以,无论一种语言是否必须优秀才能流行,我认为一种语言必须流行才能优秀。而且它必须保持流行才能保持优秀。编程语言的最新技术水平不会停滞不前。然而,我们今天拥有的 Lisp 仍然与 1980 年代中期 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 机器。但 Lisp 机器(以及并行计算机)在 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 中,我经常想遍历结构体的字段——例如,清除对已删除对象的引用,或查找未初始化的字段。我知道结构体底层只是向量。然而,我无法编写一个通用的函数来调用任何结构体。我只能按名称访问字段,因为这就是结构体应该表达的含义。

一个黑客可能只会在一个大型程序中一两次地颠覆预期的模型。但能够做到这一点会带来多大的不同啊。这可能不仅仅是解决问题。这里也有一种乐趣。黑客们分享着外科医生在粗糙内脏中探寻的秘密乐趣,青少年挤痘痘的秘密乐趣。[2] 至少对于男孩来说,某些类型的恐怖是迷人的。《Maxim》杂志每年都会出版一本包含美女照片和血腥事故混合的刊物。他们了解自己的受众。

从历史上看,Lisp 在让黑客随心所欲方面一直做得很好。Common Lisp 的“政治正确性”是一个反常现象。早期的 Lisp 让你能接触到一切。幸运的是,这种精神很大程度上保留在宏中。能够对源代码进行任意转换,这是一件多么美妙的事情啊。

经典宏是真正的黑客工具——简单、强大且危险。它们的作用很容易理解:你对宏的参数调用一个函数,然后它返回的任何内容都会插入到宏调用的位置。卫生宏体现了相反的原则。它们试图阻止你理解它们在做什么。我从未听过有人用一句话解释卫生宏。它们是决定程序员被允许想要什么这一危险的经典例子。卫生宏旨在保护我免受变量捕获等问题的影响,但变量捕获恰恰是我在某些宏中想要的。

一个真正好的语言应该既干净又“脏”:设计干净,核心由少量易于理解且高度正交的操作符组成,但“脏”在于它允许黑客随心所欲地使用它。C 就是这样。早期的 Lisp 也是。一个真正的黑客语言总是会带有一点不羁的特性。

一个好的编程语言应该具有让那些使用“软件工程”这个短语的人不赞同地摇头的特性。在这个连续体的另一端是像 Ada 和 Pascal 这样的语言,它们是规范的典范,适合教学,但除此之外用途不大。

5 一次性程序

为了吸引黑客,一种语言必须擅长编写他们想编写的程序。而这意味着,也许令人惊讶的是,它必须擅长编写一次性程序。

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

我有一种预感,最好的大型程序都是这样开始的,而不是像胡佛水坝那样从一开始就被设计成大型项目。从零开始构建大型事物令人望而生畏。当人们承担一个过大的项目时,他们会感到不知所措。项目要么陷入困境,要么结果呆板而僵硬:购物中心而非真正的市中心,巴西利亚而非罗马,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 狂热。

一个更严重的问题是前缀表示法的分散性。对于专家级黑客来说,这确实是一个问题。没有人愿意写 (aref a x y) 而不是 a[x,y]。

在这种特殊情况下,有一种巧妙地解决问题的方法。如果我们将数据结构视为索引上的函数,我们可以写 (a x y) 来代替,这甚至比 Perl 的形式更短。类似的技巧可能会缩短其他类型的表达式。

我们可以通过使缩进有意义来消除(或使其可选)许多括号。程序员无论如何都是这样阅读代码的:当缩进表示一回事而分隔符表示另一回事时,我们以缩进为准。将缩进视为有意义将消除这种常见的错误来源,并使程序更短。

有时中缀语法更容易阅读。对于数学表达式尤其如此。我整个编程生涯都在使用 Lisp,但我仍然觉得前缀数学表达式不自然。然而,拥有可以接受任意数量参数的操作符很方便,尤其是在生成代码时。所以如果我们确实有中缀语法,它可能应该作为某种读取宏来实现。

我认为我们不应该宗教般地反对在 Lisp 中引入语法,只要它能以一种易于理解的方式转换为底层的 S-表达式。Lisp 中已经有很多语法。引入更多语法不一定是坏事,只要没有人被迫使用它。在 Common Lisp 中,一些分隔符是为语言保留的,这表明至少一些设计者打算在未来拥有更多语法。

Common Lisp 中最令人发指的不 Lisp 风格的语法片段之一出现在格式字符串中;format 本身就是一种语言,而那种语言不是 Lisp。如果有一个在 Lisp 中引入更多语法的计划,格式说明符可能可以包含在其中。如果宏能够像生成其他任何类型的代码一样生成格式说明符,那将是一件好事。

一位杰出的 Lisp 黑客告诉我,他的 CLTL 副本总是翻到 format 部分。我也是。这可能表明有改进的空间。这也可能意味着程序进行了大量的 I/O。

8 效率

众所周知,一种好的语言应该生成快速的代码。但实际上,我认为快速代码主要不是来自语言设计中的做法。正如高德纳很久以前指出的那样,速度只在某些关键瓶颈处才重要。正如许多程序员后来观察到的那样,人们常常会弄错这些瓶颈在哪里。

所以,实际上,获得快速代码的方法是拥有一个非常好的性能分析器,而不是通过,比如说,使语言强类型。你不需要知道程序中每个调用的每个参数的类型。你确实需要能够在瓶颈处声明参数的类型。更重要的是,你需要能够找出瓶颈在哪里。

人们对 Lisp 的一个抱怨是很难判断什么操作开销大。这可能是真的。如果想拥有一种非常抽象的语言,这可能也是不可避免的。无论如何,我认为好的性能分析器将大大有助于解决这个问题:你很快就会知道什么操作开销大。

这里部分问题是社会性的。语言设计者喜欢编写快速编译器。那是他们衡量自己技能的方式。他们最多把性能分析器看作一个附加组件。但实际上,一个好的性能分析器在提高用该语言编写的实际程序的运行速度方面,可能比一个生成快速代码的编译器做得更多。在这里,语言设计者再次与他们的用户有些脱节。他们在解决一个稍微错误的问题上做得非常好。

拥有一个主动性能分析器可能是一个好主意——将性能数据推送给程序员,而不是等待他来索取。例如,当程序员编辑源代码时,编辑器可以用红色显示瓶颈。另一种方法是某种方式表示正在运行的程序中发生的情况。这在基于服务器的应用程序中尤其会带来巨大优势,因为你有很多正在运行的程序可以查看。一个主动性能分析器可以图形化地显示程序运行时内存中发生的情况,甚至可以发出声音来告知发生的情况。

声音是问题的好线索。在我工作的一个地方,我们有一个巨大的仪表板,显示我们的网络服务器正在发生什么。指针由微型伺服电机移动,转动时会发出轻微的噪音。我从我的办公桌看不到仪表板,但我发现我可以通过声音立即判断出服务器何时出现问题。

甚至有可能编写一个能够自动检测低效算法的性能分析器。如果某些内存访问模式被证明是糟糕算法的明确标志,我不会感到惊讶。如果计算机内部有一个小家伙在运行我们的程序,他可能会像联邦政府雇员一样,讲述一个漫长而悲惨的工作故事。我经常觉得我在让处理器做很多徒劳的追逐,但我从未有过一个好的方法来查看它在做什么。

现在许多 Lisp 都编译成字节码,然后由解释器执行。这通常是为了使实现更容易移植,但它也可以是一个有用的语言特性。将字节码作为语言的官方部分,并允许程序员在瓶颈处使用内联字节码,这可能是一个好主意。这样,这些优化也将是可移植的。

最终用户所感知的速度性质可能正在发生变化。随着基于服务器的应用程序的兴起,越来越多的程序可能会变成 I/O 密集型。使 I/O 快速将是值得的。语言可以通过简单的措施(如简单、快速、格式化的输出函数)提供帮助,也可以通过深层结构性改变(如缓存和持久化对象)提供帮助。

用户关心响应时间。但另一种效率将变得越来越重要:每个处理器可以支持的并发用户数量。未来许多有趣的应用程序将是基于服务器的,而每个服务器的用户数量是任何托管此类应用程序的人的关键问题。在提供基于服务器应用程序的业务的资本成本中,这是除数。

多年来,效率在大多数最终用户应用程序中并不重要。开发人员一直可以假设每个用户都将拥有一个越来越强大的桌面处理器。根据帕金森定律,软件已经扩展到使用可用的资源。这种情况将随着基于服务器的应用程序而改变。在那个世界里,硬件和软件将一起提供。对于提供基于服务器应用程序的公司来说,每个服务器能支持多少用户,将对其底线产生非常大的影响。

在某些应用程序中,处理器将是限制因素,执行速度将是最重要的优化目标。但通常内存将是限制;并发用户数量将由每个用户数据所需的内存量决定。语言在这里也可以提供帮助。对线程的良好支持将使所有用户共享一个堆。拥有持久化对象和/或语言层面的延迟加载支持也可能有所帮助。

9 时间

一种流行语言所需的最后一个要素是时间。没有人愿意用一种可能消失的语言编写程序,就像许多编程语言那样。所以大多数黑客会倾向于等待一种语言存在几年后才考虑使用它。

新事物的发明者常常惊讶地发现这一点,但你需要时间才能将任何信息传达给人们。我的一位朋友很少在别人第一次要求他时就做任何事。他知道人们有时会要求那些他们后来发现并不想要的东西。为了避免浪费时间,他会等到第三或第四次被要求做某事时才行动;到那时,提出要求的人可能已经相当恼火了,但至少他们可能真的想要他们所要求的东西。

大多数人已经学会了对他们听到的新事物进行类似的过滤。他们甚至要等到听到某件事十次之后才开始关注。他们完全有理由这样做:大多数热门新事物最终都被证明是浪费时间,并最终消失。通过延迟学习 VRML,我完全避免了学习它的必要。

所以任何发明新事物的人都必须预料到需要多年重复他们的信息,人们才会开始理解。我们编写了据我所知第一个基于网络服务器的应用程序,我们花了数年时间才让人们明白它不需要下载。并不是他们愚蠢。他们只是把我们“屏蔽”了。

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

通常需要一段时间才能获得动力,这也很正常。大多数技术即使在首次推出后也会有很大的发展——编程语言尤其如此。对于一项新技术来说,被少数早期采用者使用几年,没有什么比这更好的了。早期采用者老练且要求严格,他们会迅速清除你的技术中残留的任何缺陷。当你只有少数用户时,你可以与他们所有人保持密切联系。当你改进系统时,即使这会导致一些兼容性问题,早期采用者也会宽容。

新技术的引入有两种方式:有机增长法和大爆炸法。有机增长法的典型例子是经典的、资金不足的、白手起家的车库创业公司。几个人默默无闻地工作,开发出一些新技术。他们没有营销就推出了它,最初只有少数(狂热的忠实)用户。他们不断改进技术,同时他们的用户群通过口碑增长。不知不觉中,他们就壮大了。

另一种方法,大爆炸法,其典型例子是风险投资支持、大肆营销的初创公司。他们匆忙开发产品,大张旗鼓地推出,并立即(他们希望)拥有庞大的用户群。

通常,车库里的家伙们会羡慕大爆炸的家伙们。大爆炸的家伙们圆滑自信,受到风险投资家的尊敬。他们能负担得起最好的东西,而发布会周围的公关活动附带地让他们成为了名人。有机增长的家伙们坐在车库里,感到贫穷且无人爱。然而,我认为他们常常不应该自怨自艾。有机增长似乎比大爆炸法产生更好的技术和更富有的创始人。如果你看看今天的主导技术,你会发现它们大多数都是有机增长的。

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

10 重新设计

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

要编写好的软件,你必须同时在脑海中保持两个对立的想法。你需要年轻黑客对自己能力的幼稚信念,同时又需要老兵的怀疑精神。你必须能够用大脑的一半思考这能有多难?,同时用另一半思考这永远行不通

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

做得好的人常常认为他们正在做的东西不好。其他人看到他们所做的,充满了惊奇,但创造者却充满了担忧。这种模式并非巧合:正是这种担忧让作品变得优秀。

如果你能让希望和担忧保持平衡,它们就会像你的两条腿驱动自行车前进一样,推动项目向前发展。在双循环创新引擎的第一阶段,你被解决问题的信心所鼓舞,努力工作。在第二阶段,你在清晨的冷光下审视你所做的一切,清晰地看到所有缺陷。但只要你的批判精神没有超过你的希望,你就能审视你公认不完善的系统,并思考,剩下的路能有多难?,从而继续这个循环。

保持这两种力量的平衡很棘手。在年轻黑客中,乐观主义占主导地位。他们做出一些东西,确信它很棒,然后从不改进。在老黑客中,怀疑主义占主导地位,他们甚至不敢承担雄心勃勃的项目。

任何能让重新设计循环持续下去的事情都是好的。散文可以一遍又一遍地重写,直到你满意为止。但软件通常没有得到足够的重新设计。散文有读者,但软件有_用户_。如果一个作家重写一篇散文,阅读旧版本的人不太可能抱怨他们的思路被新引入的不兼容性打断了。

用户是一把双刃剑。他们可以帮助你改进语言,但他们也可以阻止你改进它。所以要仔细选择你的用户,并缓慢增加他们的数量。拥有用户就像优化一样:明智的做法是推迟它。此外,作为一般规则,你在任何给定时间都可以改变比你想象的更多。引入改变就像撕掉创可贴:痛苦几乎在你感觉到它的那一刻就成了记忆。

每个人都知道由委员会设计语言不是一个好主意。委员会导致糟糕的设计。但我认为委员会最糟糕的危险是它们阻碍重新设计。引入改变的工作量太大,以至于没有人愿意费心。委员会决定的任何事情都倾向于保持原样,即使大多数成员不喜欢它。

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

这里的一个解决方案可能是设计系统,使接口是水平而非垂直的——这样模块总是垂直堆叠的抽象层。那么接口将倾向于由其中一方拥有。两个级别中较低的一个要么是上层编写的语言,在这种情况下较低级别将拥有接口,要么它将是从属的,在这种情况下接口可以由上层决定。

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 并理解它的人。而这些正是你需要赢得的用户。

在《如何成为一名黑客》中,Eric Raymond 将 Lisp 描述为类似于拉丁语或希腊语——一种你应该作为智力锻炼来学习的语言,即使你实际上不会经常使用它:

Lisp 值得学习,因为它会给你带来深刻的启示体验,当你最终掌握它时;这种体验将使你余生都成为更好的程序员,即使你实际上很少使用 Lisp 本身。

如果我不知道 Lisp,读到这段话会让我提出问题。一种能让我成为更好的程序员的语言,如果它有任何意义的话,就意味着它更适合编程。而这实际上就是 Eric 所说的含义。

只要这个想法还在流传,我认为黑客们会对一个新的 Lisp 足够接受,即使它被称为 Lisp。但这个 Lisp 必须是黑客的语言,就像 1970 年代的经典 Lisp 一样。它必须简洁、简单且可修改。而且它必须拥有强大的库,用于完成黑客现在想做的事情。

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

拥有对基于服务器应用程序的核心语言支持可能会带来更大的优势。例如,对多用户程序的明确支持,或类型标签级别的数据所有权。

基于服务器的应用程序也为我们提供了这个新的 Lisp 将被用来“黑”什么的问题的答案。让 Lisp 作为 Unix 的脚本语言变得更好并无坏处。(要让它变得更糟也很难。)但我认为在某些领域,现有语言更容易被击败。我认为最好遵循 Tcl 的模式,将 Lisp 与一个支持基于服务器应用程序的完整系统一起提供。Lisp 天然契合基于服务器的应用程序。当 UI 只是一系列网页时,词法闭包提供了一种实现子程序效果的方法。S-表达式可以很好地映射到 HTML,而宏擅长生成它。需要更好的工具来编写基于服务器的应用程序,也需要一个新的 Lisp,两者将很好地协同工作。

12 梦想语言

作为总结,让我们尝试描述一下黑客的梦想语言。梦想语言是优美、简洁、精炼的。它有一个快速启动的交互式顶层。你可以用很少的代码编写程序来解决常见问题。你编写的任何程序中,几乎所有代码都是特定于你的应用程序的代码。其他一切都已为你完成。

该语言的语法简洁到极致。你永远不必输入不必要的字符,甚至不必过多使用 Shift 键。

使用大型抽象,你可以非常快速地编写程序的第一个版本。之后,当你想优化时,有一个非常好的性能分析器会告诉你将注意力集中在哪里。你可以让内层循环快如闪电,甚至在需要时编写内联字节码。

有很多好的例子可以学习,而且该语言足够直观,你可以在几分钟内从例子中学会如何使用它。你不需要过多地查阅手册。手册很薄,警告和限定很少。

该语言有一个小核心,以及强大、高度正交的库,这些库像核心语言一样精心设计。所有库都协同工作良好;语言中的一切都像精密相机中的零件一样完美契合。没有任何东西被废弃,或为兼容性而保留。所有库的源代码都随时可用。它很容易与操作系统以及用其他语言编写的应用程序交互。

该语言是分层构建的。更高级别的抽象以非常透明的方式从更低级别的抽象构建,如果你愿意,可以获取这些低级别抽象。

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

注释

[1] 1964 年,即 Lisp 1.5 发布两年后,Timothy Hart 提出了非常接近现代概念的宏。最初缺少的是避免变量捕获和多次求值的方法;Hart 的例子都受这两者影响。

[2] 在《当空气进入你的大脑》一书中,神经外科医生 Frank Vertosick 讲述了一段对话,其中他的首席住院医师 Gary 谈论了外科医生和内科医生(“跳蚤”)之间的区别:

Gary 和我点了一个大披萨,找了一个空位。首席住院医师点燃了一支烟。“看看那些该死的跳蚤,喋喋不休地谈论他们一生中只会遇到一次的疾病。跳蚤的问题就在于,他们只喜欢稀奇古怪的东西。他们讨厌日常案例。这就是我们和那些该死的跳蚤的区别。你看,我们喜欢大而多汁的腰椎间盘突出,但他们讨厌高血压……”

很难将腰椎间盘突出想象成“多汁的”(除了字面上)。但我认为我明白他们的意思。我经常有一个“多汁”的 bug 要追查。一个非程序员会觉得很难想象 bug 中会有乐趣。当然,如果一切都正常工作会更好。从某种意义上说,确实如此。然而,追查某些类型的 bug 确实有一种阴沉的满足感。