受欢迎的原因
Original2001年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的语法出现在格式字符串中;格式本身是一种独立的语言,而那种语言并不是Lisp。如果有计划在Lisp中引入更多语法,格式说明符可能能够被包含在内。如果宏能够生成格式说明符,就像它们生成任何其他类型的代码一样,那将是件好事。
一位著名的Lisp黑客告诉我,他的CLTL副本总是翻到格式部分。我也是。这可能表明有改进的空间。这也可能意味着程序进行大量I/O。
8 效率
众所周知,一种好的语言应该生成快速的代码。但实际上,我认为快速的代码主要不是来自于你在语言设计中所做的事情。正如Knuth很久以前指出的,速度只在某些关键瓶颈中才重要。正如许多程序员自那时以来观察到的,关于这些瓶颈的位置,人们常常会犯错误。
因此,在实践中,获得快速代码的方式是拥有一个非常好的分析器,而不是,比如说,使语言强类型。你不需要知道程序中每个调用的每个参数的类型。你确实需要能够在瓶颈处声明参数的类型。更重要的是,你需要能够找出瓶颈在哪里。
人们对Lisp的一个抱怨是,很难判断什么是昂贵的。这可能是真的。如果你想拥有一种非常抽象的语言,这也可能是不可避免的。无论如何,我认为良好的分析会在很大程度上解决这个问题:你很快就会了解到什么是昂贵的。
这里的问题部分是社会性的。语言设计者喜欢编写快速的编译器。这是他们衡量自己技能的方式。他们认为分析器充其量只是一个附加功能。但在实践中,一个好的分析器可能比生成快速代码的编译器更能提高用该语言编写的实际程序的速度。在这一点上,语言设计者与他们的用户有些脱节。他们在解决稍微错误的问题上做得很好。
拥有一个主动的分析器可能是个好主意——将性能数据推送给程序员,而不是等他来询问。例如,当程序员编辑源代码时,编辑器可以用红色显示瓶颈。另一种方法是以某种方式表示正在运行的程序中发生的事情。这在基于服务器的应用程序中尤其有利,因为你有很多正在运行的程序可以查看。一个主动的分析器可以图形化地显示程序运行时内存中发生的事情,甚至可以发出声音来告诉你发生了什么。
声音是问题的一个良好提示。在我工作过的一个地方,我们有一个大仪表板,显示我们的网络服务器发生了什么。指针由小伺服电机移动,当它们转动时会发出轻微的噪音。我无法从我的桌子看到仪表板,但我发现我可以通过声音立即判断服务器是否出现问题。
甚至可能编写一个分析器,自动检测低效算法。如果某些内存访问模式最终被证明是糟糕算法的确切迹象,我不会感到惊讶。如果有一个小家伙在计算机内部执行我们的程序,他可能会有一段长长而悲伤的故事要讲述,就像一名联邦政府雇员一样。我常常有一种感觉,我在让处理器进行许多无用的追逐,但我从未有过好的方法来查看它在做什么。
现在有许多Lisp编译成字节码,然后由解释器执行。这通常是为了使实现更容易移植,但这可能是一个有用的语言特性。将字节码作为语言的官方部分,并允许程序员在瓶颈中使用内联字节码,可能是个好主意。这样,这些优化也将是可移植的。
速度的性质,作为最终用户所感知的,可能正在发生变化。随着基于服务器的应用程序的兴起,越来越多的程序可能会变得I/O绑定。使I/O快速将是值得的。语言可以通过简单、快速、格式化的输出函数等直接措施来帮助,也可以通过深层结构变化,如缓存和持久对象来帮助。
用户对响应时间感兴趣。但另一种效率将变得越来越重要:每个处理器可以支持的同时用户数量。未来编写的许多有趣应用程序将是基于服务器的,而每个服务器的用户数量是任何托管此类应用程序的关键问题。在提供基于服务器的应用程序的企业的资本成本中,这就是除数。
多年来,在大多数最终用户应用程序中,效率并不重要。开发人员可以假设每个用户的桌面上都有一个越来越强大的处理器。根据帕金森法则,软件已经扩展到使用可用的资源。在基于服务器的应用程序的世界中,这将发生变化。在那个世界中,硬件和软件将一起提供。对于提供基于服务器的应用程序的公司来说,他们每台服务器可以支持多少用户将对底线产生非常大的影响。
在某些应用程序中,处理器将是限制因素,执行速度将是最重要的优化内容。但通常内存将是限制;同时用户的数量将由每个用户数据所需的内存量决定。语言在这里也可以提供帮助。对线程的良好支持将使所有用户能够共享一个堆。拥有持久对象和/或语言级别的延迟加载支持也可能有所帮助。
9 时间
一种流行语言所需的最后一个成分是时间。没有人想在一种可能消失的语言中编写程序,正如许多编程语言所做的那样。因此,大多数黑客往往会等到一种语言存在几年后,才会考虑使用它。
发明奇妙新事物的人常常会惊讶地发现这一点,但你需要时间才能将任何信息传达给人们。我的一个朋友很少在有人第一次问他时就做任何事情。他知道人们有时会要求他们最终并不想要的东西。为了避免浪费时间,他会等到第三或第四次被要求做某事;到那时,问他的人可能会相当恼火,但至少他们可能真的想要他们所要求的东西。
大多数人已经学会对他们听到的新事物进行类似的过滤。他们甚至在听到某件事情十次之前都不会开始关注。这样做是完全合理的:大多数热门新事物最终确实会浪费时间,并最终消失。通过推迟学习VRML,我避免了必须学习它。
因此,任何发明新事物的人都必须预期在几年内不断重复他们的信息,才能让人们开始理解。我们编写了一个据我所知的第一个基于网络服务器的应用程序,花了我们几年时间才让人们明白它不必被下载。并不是说他们愚蠢。他们只是将我们调出了。
好消息是,简单的重复解决了这个问题。你所要做的就是不断讲述你的故事,最终人们会开始听到。人们注意到你在那里的时候并不意味着他们会关注;而是当他们注意到你仍然在那里的时候。
通常,获得动力确实需要一段时间。大多数技术在首次推出后仍会经历相当大的演变——尤其是编程语言。对于新技术来说,没有什么比在最初几年仅被少数早期采用者使用更好的了。早期采用者是复杂且要求严格的,他们迅速发现你技术中仍然存在的缺陷。当你只有少数用户时,你可以与他们所有人保持密切联系。而且早期采用者在你改进系统时会宽容,即使这会导致一些破坏。
新技术的引入有两种方式:有机增长法和大爆炸法。有机增长法的典型例子是经典的自发性、资金不足的车库初创公司。几个家伙在默默无闻中开发一些新技术。他们在没有市场营销的情况下推出,最初只有少数(狂热的)用户。他们继续改进技术,同时他们的用户基础通过口耳相传而增长。在他们意识到之前,他们已经变得庞大。
另一种方法,大爆炸法,以风险投资支持、市场营销强劲的初创公司为典型。他们急于开发产品,借助巨大的宣传推出,并立即(他们希望)拥有一个庞大的用户基础。
通常,车库公司会羡慕大爆炸公司。大爆炸公司光鲜亮丽,自信,受到风险投资者的尊重。他们可以负担得起最好的所有东西,围绕推出的公关活动也使他们成为名人。坐在车库里的有机增长公司感到贫穷和被冷落。然而,我认为他们常常错在自怜。有机增长似乎比大爆炸法产生更好的技术和更富有的创始人。如果你看看今天主导的技术,你会发现它们大多数都是有机增长的结果。
这种模式不仅适用于公司。你在赞助研究中也会看到它。Multics和Common Lisp是大爆炸项目,而Unix和MacLisp是有机增长项目。
10 重新设计
“最好的写作是重写,”E.B.怀特写道。每个优秀的作家都知道这一点,这对软件也是如此。设计中最重要的部分是重新设计。编程语言,尤其是,往往没有得到足够的重新设计。
要编写好的软件,你必须同时在脑海中保持两个对立的想法。你需要年轻黑客对自己能力的天真信念,同时又需要老练者的怀疑。你必须能够用大脑的一半思考这有多难?,而用另一半思考这永远不会成功。
诀窍在于意识到这里没有真正的矛盾。你希望对两件不同的事情保持乐观和怀疑。你必须对解决问题的可能性保持乐观,但对你迄今为止所拥有的任何解决方案的价值保持怀疑。
做出好工作的人往往认为他们正在做的事情没有价值。其他人看到他们所做的事情时充满惊奇,但创造者却充满担忧。这种模式并非巧合:正是这种担忧使得工作变得优秀。
如果你能保持希望和担忧的平衡,它们将像你两条腿推动自行车一样推动项目向前发展。在两周期创新引擎的第一阶段,你在某个问题上拼命工作,受到你能解决它的信心的激励。在第二阶段,你在清晨的冷光下审视你所做的事情,清晰地看到它的所有缺陷。但只要你的批判精神没有超过你的希望,你就能看着你承认不完整的系统,想,解决剩下的部分有多难?从而继续这个循环。
保持这两种力量的平衡是棘手的。在年轻黑客中,乐观占主导地位。他们产生了一些东西,确信它很棒,并且从不改进它。在老练的黑客中,怀疑占主导地位,他们甚至不敢承担雄心勃勃的项目。
你能做的任何事情来保持重新设计周期的进行都是好的。散文可以反复重写,直到你满意为止。但软件通常没有得到足够的重新设计。散文有读者,但软件有用户。如果一个作家重写了一篇文章,阅读旧版本的人不太可能抱怨他们的想法因某种新引入的不兼容而被打破。
用户是把双刃剑。他们可以帮助你改进语言,但也可能阻止你改进它。因此,仔细选择你的用户,并缓慢增加他们的数量。拥有用户就像优化:明智的做法是推迟它。此外,作为一般规则,你在任何给定时间都可以改变的事情比你想象的要多。引入变化就像撕掉绷带:疼痛几乎在你感受到它的瞬间就成为记忆。
众所周知,由委员会设计语言并不是一个好主意。委员会产生糟糕的设计。但我认为委员会的最大危险在于它们干扰重新设计。引入变化是如此繁琐,以至于没有人愿意去麻烦。委员会决定的事情往往保持不变,即使大多数成员并不喜欢它。
即使是两个委员会也会妨碍重新设计。这种情况尤其发生在由两个不同的人编写的软件之间的接口中。要更改接口,双方必须同时同意更改。因此,接口往往根本不会改变,这很成问题,因为它们往往是任何系统中最临时的部分之一。
这里的一种解决方案可能是设计系统,使接口是水平的而不是垂直的——使模块始终是垂直堆叠的抽象层次。然后,接口将倾向于由其中一个拥有。两个层次中较低的层将是上层编写的语言,在这种情况下,较低的层将拥有接口,或者它将是一个从属层,在这种情况下,接口可以由上层决定。
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非常适合基于服务器的应用程序。词法闭包提供了一种在用户界面仅为一系列网页时获得子例程效果的方法。S表达式与html很好地映射,宏在生成html方面表现良好。需要更好的工具来编写基于服务器的应用程序,也需要一个新的Lisp,这两者将很好地结合在一起。
12 梦想语言
总结一下,让我们尝试描述黑客的梦想语言。梦想语言是美丽的、干净且简洁的。它有一个快速启动的交互式顶层。你可以用很少的代码编写程序来解决常见问题。你编写的任何程序中的几乎所有代码都是特定于你的应用程序的。其他一切都已经为你完成。
语言的语法简洁到过分。你永远不需要输入不必要的字符,甚至不需要经常使用Shift键。
使用大型抽象,你可以非常快速地编写程序的第一个版本。稍后,当你想要优化时,有一个非常好的分析器可以告诉你关注的重点。你可以让内部循环变得极快,甚至在需要时编写内联字节码。
有很多好的示例可以学习,语言直观到足以让你在几分钟内从示例中学习如何使用它。你不需要经常查阅手册。手册很薄,几乎没有警告和限制。
语言有一个小核心,以及强大且高度正交的库,这些库的设计与核心语言一样精心。所有库都能很好地协同工作;语言中的一切都像精密相机中的部件一样完美契合。没有任何东西被弃用或保留以兼容性。所有库的源代码都可以轻松获取。与操作系统和用其他语言编写的应用程序的交互也很简单。
语言是分层构建的。更高层次的抽象是以非常透明的方式从较低层次的抽象构建的,如果你想的话,你可以获取这些较低层次的抽象。
没有任何不绝对必要的东西会被隐藏。语言提供抽象只是为了节省你的工作,而不是为了告诉你该做什么。实际上,语言鼓励你作为其设计的平等参与者。你可以改变它的所有内容,包括它的语法,尽可能地使你编写的任何东西与预定义的内容具有相同的地位。
注释
[1] 与现代概念非常接近的宏是由Timothy Hart在1964年提出的,Lisp 1.5发布两年后。最初缺少的是避免变量捕获和多次评估的方法;Hart的示例都受到这两者的影响。
[2] 在《当空气撞击你的大脑》中,神经外科医生Frank Vertosick讲述了一次对话,其中他的首席住院医生Gary谈到了外科医生和内科医生(“跳蚤”)之间的区别:
Gary和我点了一大份披萨,找了一个空的包厢。首席点燃了一根香烟。“看看那些该死的跳蚤,喋喋不休地谈论他们一生中只会见到一次的某种疾病。这就是跳蚤的问题,他们只喜欢奇怪的东西。他们讨厌他们的主业案例。这就是我们和该死的跳蚤之间的区别。你看,我们喜欢大而多汁的腰椎间盘突出,但他们讨厌高血压……"
很难把腰椎间盘突出想象成多汁的(除了字面意义)。然而,我想我知道他们的意思。我常常有一个多汁的bug需要追踪。一个不是程序员的人很难想象bug中会有乐趣。肯定如果一切都能正常工作会更好。从某种意义上说,确实如此。然而,追踪某些类型的bug无疑会带来一种严峻的满足感。