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语言中“本身就提供,或者被大大简化”。(m/design-pattems)」14.梦寐以求的编程语言一心让臣民行善的暴君可能是最专制的暴君。——C.S.LEWIS(1898—1963,英国小说家) 我的朋友曾对一位著名的操作系统专家说他想要设计一种真正优秀的编程语言。那位专家回答,这是浪费时间,优秀的语言不一定会被市场接受,很可能无人使用,因为语言的流行不取决于它本身。至少,那位专家设计的语言就遭遇到了这种情况。 那么,语言的流行到底取决于什么因素呢?流行的语言是否真的值得流行呢?还有必要尝试设计一种更好的语言吗?如果有必要的话,怎样才能做到这一点呢? 为了找到这些问题的答案,我想我们可以观察黑客,了解他们使用什么语言。编程语言本来就是为了满足黑客的需要而产生的,当且仅当黑客喜欢一种语言时,这种语言才能成为合格的编程语言,而不是被当作“指称语义”(denotational semantics)或者编译器设计。流行的秘诀 没错,大多数人选择某一种编程语言,不是因为这种语言有什么独特的特点,而是因为听说其他人使用这种语言。但是我认为,外界因素对于编程语言的流行其实没有想象中那么大的影响力。我倒是觉得,问题出在对于什么是优秀编程语言,黑客的看法与大多数的语言设计者不一样。 黑客的看法其实比语言设计者的更重要。编程语言不是数学定理,而是一种工具,为了便于使用,它们才被设计出来。所以,设计编程语言的时候必须考虑到人类的长处和短处,就像设计鞋子的时候必须符合人类的脚型。如果鞋子穿上去不舒服,无论它的外形多么优美,多么像一件艺术品,你也只能把它当作一双坏鞋。 大多数程序员也许无法分辨语言的好坏。但是,这不代表优秀的编程语言会被埋没,专家级黑客一眼就能认出它们,并且会拿来使用。虽然他们人数很少,但就是这样一小群人写出了人类所有优秀软件。他们有着巨大的影响力,他们使用什么语言,其他程序员往往就会跟着使用。老实说,很多时候这种影响力更像是一种命令,对于其他程序员来说,专家级黑客就像自己的老板或导师,他们说哪种语言好用,自己就会乖乖地跟进。 专家级黑客的看法不是决定一种语言流行程度的唯一因素,某些古老的软件(Fortran和Cobol的情况)和铺天盖地的广告宣传(Ada和Java的情况)也会起到作用。但是,我认为从长期来看,专家级黑客的看法是最重要的因素。只要有了达到“临界数量”(critical mass)的最初用户和足够长的时间,一种语言可能就会达到应有的流行程度。而流行本身又会使得这种优秀的语言更加优秀,进一步拉大它与平庸语言之间的好坏差异,因为使用者的反馈总是会导致语言的改进。你可以想一下,所有流行的编程语言从诞生至今的变化有多大。Perl和Fortran是极端的例子,除它们两个之外,甚至就连Lisp都发生了很大的变化。 所以,即使不考虑语言本身的优秀是否能带动流行,我想单单流行本身就肯定会使得这种语言变得更好,只有流行才会让它保持优秀。编程语言的最高境界一直在发展之中。虽然语言的核心功能就像大海的深处,很少有变化,但是函数库和开发环境之类的东西就像大海的表面,一直在汹涌澎湃。 当然,黑客必须先知道这种语言,才可能去用它。他们怎么才能知道呢?就是从其他黑客那里。所以不管怎样,一开始必须有一群黑客使用这种语言,然后其他人才会知道它。我不知道“一群”的最小数量是多少,多少个黑客才算达到“临界数量”呢?如果让我猜,我会说20人。如果一种语言有20个独立用户,就意味这20个人是自主决定使用这种语言的,我觉得这就说明这种语言真的有优点。 达到这一步并非易事。如果说用户数从0到20比从20到1000更困难,我也不会感到惊讶。发展最早的20个用户的最好方法可能就是使用特洛伊木马:你让人们使用一种他们需要的应用程序,这个程序偏巧就是用某种新语言开发的。外部因素 我们得先承认,确实有一个外部因素会影响到语言的流行。一种语言必须是某一个流行的计算机系统的脚本语言(scripting language),才会变得流行。Fortran和Cobol是早期IBM大型机的脚本语言。C是Unix的脚本语言,后来的Perl和Python也是如此。Tel是Tk的脚本语言,Visual Basic是Windows的脚本语言,(某种形式的)Lisp是Emacs的脚本语言,PHP是网络服务器的脚本语言,Java和JavaScript是浏览器的脚本语言。 编程语言不是存在于真空之中。“编程”其实是及物动词,黑客一般都是为某个系统编程,在现实中,编程语言总是与它们依附的系统联系在一起的。所以,如果你想设计一种流行的编程语言,就不能只是单纯地设计语言本身,还必须为它找到一个依附的系统,而这个系统也必须流行。除非你只想用自己设计的语言取代那个系统现有的脚本语言。 这种情况导致的一个结果就是,无法以一种语言本身的优缺点评判这种语言。另一个结果则是,只有当一种语言是某个系统的脚本语言时,它才能真正成为编程语言。如果你对此很吃惊,觉得不公平,那么我会跟你说不必大惊小怪。这就好比大家都认为,如果一种编程语言只有语法规则,没有一个好的实现(implementation),那么它就不能算完整的编程语言。这些都是很正常很合理的事情,编程语言本来就该如此。 当然,编程语言本来就需要一个好的实现,而且这个实现必须是免费的。商业公司愿意出钱购买软件,但是黑客作为个人不会愿意这样做,而你想让一种语言成功,恰恰就是需要吸引黑客。 编程语言还需要有一本介绍它的书。这本书应该不厚,文笔流畅,而且包含大量优秀的范例。布赖恩·柯尼汉和丹尼斯·里奇合写的《C程序设计语言》(C Programming Language)就是这方面的典范。眼下,我大槪还能再加一句,这一类书籍之中必须有一本由O'Reilly公司出版发行。这正在变成是否能吸引黑客的前提条件了。 编程语言还应该有在线文档。事实上,在线文档可以当作一本书来写,但是目前它还无法取代实体书。实体书并没有过时,它们读起来很方便,而且出版社对书籍内容的审核是一种很有用的质量保证机制(虽然做得很不完美)。书店则是程序员发现和学习新语言的最重要的场所之一。简洁 假定你的语言已经能够满足上面三项条件——一种免费的实现,一本相关书籍,以及语言所依附的计算机系统——那么还需要做什么才能使得黑客喜欢上你的语言? 黑客欣赏的一个特点就是简洁。黑客都是懒人,他们同数学家和现代主义建筑师一样,痛恨任何冗余的东西或事情。有一个笑话说,黑客动手写程序之前,至少会在心里盘算一下哪种语言的打字工作量最小,然后就选择使用该语言。这个笑话其实与真实情况相差无几。就算这真的是个笑话,语言的设计者也必须把它当真,按照它的要求设计语言。 简洁性最重要的方面就是要使得语言更抽象。为了达到这一点,首先你设计的必须是高级语言,然后把它设计得越抽象越好。语言设计者应该总是看着代码,问自己能不能使用更少的语法单位把它表达出来。如果你有办法让许多不同的程序都能更简短地表达出来,那么这很可能意味着你发现了一种很有用的新抽象方法。 不要觉得为用户着想就是让他们使用像英语一样又长又啰嗦的语法。这是不正确的做法,Cobol就是因为这个毛病而声名狼藉。如果你让黑客像下面这样求和:add x to y giving z 而不是写成:z=x+y 那么你就是在侮辱黑客的智商,或者自己作孽了。 简洁性是静态类型语言的力所不及之处。不考虑其他因素时,没人愿意在程序的头部写上一大堆的声明语句。只要计算机可以自己推断出来的事情,都应该让计算机自己去推断。举例来说,hello-world本应该是一个很简单的程序,但是在Java语言中却要写上一大堆东西,这本身就差不多可以说明Java语言设计得有问题了^。^「hello-world程序的唯一作用就是显示出“Hello,world!”这句话。使用Java语言,你需要这样写:public class Hello {public static void main(String[] args) {Sintln("Hello, world!");}}如果你从来没有接触过编程,看到上面的代码可能会很奇怪,让计算机显示一句话为什么要镐得这么复杂?有意思的是,资深程序员的反应与你一样。」 单个的语法单位也应该很简短。Perl和Common Lisp在这方面是两个不同的极端。Perl的语法单位很短,导致它的代码可以拥挤得让人无法理解,而Common Lisp内置运算符的名称则长得可笑。Common Lisp的设计者们可能觉得文本编辑器会帮助用户自动填写运算符的长名称。但是这样做的代价不仅是增加了打字的工作量,还包括提高了阅读代码的难度,以及占用了更多的显示器空间。可编程性(Hackability) 对黑客来说,选择编程语言的时候,还有一个因素比简洁更重要,那就是这种语言必须能够帮助自己做到想做的事。在编程语言的历史上,防止程序员做出“错误”举动的措施多得惊人。这是语言设计者很自以为是的危险举动,他们怎么知道程序员该做什么不该做什么?我认为,语言设计者应该假定他们的目标用户是一个天才,会做出各种他们无法预知的举动,而不是假定目标用户是一个笨手笨脚的傻瓜,需要别人的保护才不会伤到自己。如果用户真的是傻瓜,不管你怎么保护他,他还是会搬起石头砸自己的脚。你也许能够阻止他引用另一个模块中的变量,但是你没法防止他日日夜夜不知疲倦地写出结构混乱的程序去解决完全错误的问题。 优秀程序员经常想做一些既危险又令人恼火的事情。所谓“令人恼火”,我指的是他们会突破设计者提供给用户的外部语义层,试着控制某些高级抽象的语言内部接口。比如,黑客喜欢破解,而破解就意味着深入内部,揣测原始设计者的意图。 你应该敞开胸怀,欢迎这种揣测,对于制造工具的人来说,总是会有用户以违背你本意的方式使用你的工具。如果你制造的是编程语言这样高度组合的系统,那就更是如此了。许多黑客会用你做梦也想不到的方式改动你的语法模型。我的建议就是,让他们这样干吧,而且应该为他们创造便利,尽可能多地把语言的内部暴露在他们面前。 其实,黑客并不会彻底颠覆你的工具,在一个大型程序中,他可能只是对语言改造一两个地方。但是,改动多少地方并不重要,重要的是他能够对语言进行改动。这可能不仅有助于解决一些特殊的问题,还会让黑客觉得很好玩。黑客改造语言的乐趣就好比外科医生摆弄病人内脏的乐趣,或者青少年喜欢用手挤破青春痘的那种感觉。至少对男生来说,某些类型的破坏非常刺激。针对青年男性读者的Maxim杂志每年出版一本特辑,里面一半是美女照片,另一半是各种严重事故的现场照片。这本杂志非常清楚它的读者想看什么^。^「在《神经外科医生手记》(When the Air Hits Your Brain)一书中,神经外科医生弗托塞克讲述了住院总医生戈雷的一段话,内容关于外科医生与内科医生的区别。戈雷和我要了一个大比萨,找了一张空桌子坐下。他点起一根香烟,说:“那些内科医生真是令人讨厌,总是喜欢谈论一辈子只能遇到一次的病例。这就是他们的问题,他们只喜欢古怪的东西,讨厌普通的常见病例。这就是我们和他们的区别。你看,我们喜欢腰椎间盘突出,觉得像比萨一样又大又好吃,但是他们看到高血压就憎恨不已……”很难把腰椎间盘突出与又大又好吃联系在一起,徂是,我想我知道他们指的是什么。我经常觉得某个bug非常诱人,一定要追踪下去。不是程序员的人很难想象bug有什么好玩的。一切正常当然很好,但是不可否认,能够抓到某些bug会让人兴奋到极点。」 一种真正优秀的编程语言应该既整洁又混乱。“整洁”的意思是设计得很清楚,内核由数量不多的运算符构成,这些运算符易于理解,每一个都有很完整的独立用途。“混乱”的意思是它允许黑客以自己的方式使用。C语言就是这样的例子,早期的Lisp语言也是如此。真正的黑客语言总是稍微带一点放纵不羁、不服管敎的个性。 优秀的编程语言所具备的功能,应该会使得言必称“软件工程”的人感到非常不满、频频摇头。与黑客语言形成鲜明对照的就是像Pascal那样的语言,它是井然有序的模范,非常适合教学,但是除此之外就没有很大用处了。一次性程序 为了吸引黑客,一种编程语言必须善于完成黑客想要完成的各种任务。这意味着它必须很适合开发一次性程序。这一点可能出乎很多人的意料。 所谓一次性程序,就是指为了完成某些很简单的临时性任务而在很短时间内写出来的程序。比如,自动完成某些系统管理任务的程序,或者(为了某项模拟任务)自动生成测试数据的程序,以及在不同格式之间转化数据的程序等。令人吃惊的是,一次性程序往往不是真的只用一次,就像二战期间很多美国大学造的一大批临时建筑后来都成了永久建筑。许多一次性程序后来也都变成了正式的程序,具备了正式的功能和外部用户。 我有一种预感,最优秀的那些大型程序就是这样发展起来的,而不是像胡佛水坝那样从一开始就作为大型工裎来设计。一下子从无到有做出一个大项目是很恐怖的一件事。当人们接手一个巨型项目时,很容易被它搞得一蹶不振。最后,要么是项目陷入僵局,要么是做出来一个规模小、性能差的东西。你想造一片闹市,却只做出一家商场;你想建一个罗马,却只造出一个巴西利亚;你想发明C语言,却只开发出Ada。 开发大型程序的另一个方法就是从一次性程序开始,然后不断地改进。这种方法比较不会让人望而生畏,程序在不断的开发之中逐渐进步。一般来说,使用这种方法开发程序,一开始用什么编程语言,就会一直用到最后,因为除非有外部政治因素的干预,程序员很少会中途更换编程语言。所以,我们就有了一个看似矛盾的结论:如果你想设计一种适合开发大型项目的编程语言,就必须使得这种语言也适合开发一次性程序,因为大型项目就是从一次性程序演变而来的。 Perl就是一个鲜明的例子。它不仅仅设计成适合开发一次性程序,而且它本身就很像一次性程序。最初的Perl只是好几个生成表格的工具收集在一起而已。后来程序员用它写一次性程序,当那些程序逐渐发展壮大后,Perl才随之发展成了一种正式的编程语言。到了Perl 5,这种语言才适合开发重要的程序,但是在此之前它已经广为流行了。 什么样的语言适合写一次性程序?首先,它必须很容易装备。一次性程序是你只想在一小时内写出来的程序,所以它不应该耗费很多时间安装和配置,最好已经安装在你的电脑上了。它必须是想用就用的。C语言可以想用就用,因为它是操作系统的一部分;Perl可以想用就用,因为它本来就是一种系统管理工具,操作系统已经默认安装它了。 很容易装备不仅仅指很容易安装或者已经安装,还指很容易与使用者互动。一种有命令行界面、可以实时反馈的语言就具有互动性,那些必须先编译后使用的语言就不具备互动性。受欢迎的编程语言应该是前者,具有良好的互动性,可以快速得到运行结果。 一次性程序的另一个特点就是简洁。对黑客来说,这一点永远有吸引力。如果考虑到你最多只打算在这个程序上耗费一个小时,这一点就更重要了。函数库 简洁性的最高形式当然是有人已经帮你把程序写好,你只要运行就可以了。函数库就是别人帮你写好的程序,所以它是编程语言的另一个重要特点,并且我认为正在变得越来越重要。Perl就赢在它具有操作字符串的巨大函数库。这类函数库对一次性程序特别重要,因为开发一次性程序的原始目的往往就是转化或提取字符串。许多Perl程序的原型可能就是把几个函数库调用放在一起。 我认为,未来50年中,编程语言的进步很大一部分与函数库有关。未来的函数库将像语言内核一样精心设计。优秀函数库的重要性将超过语言本身。某种语言到底是静态类型还是动态类型、是面向对象还是函数式编程,这些都不如函数库重要。那些习惯用变量类型考虑问题的语言设计者可能会对这种趋势感到不寒而栗。这不等于把语言设计降到开发应用程序的层次吗?哦,真是太糟了。但是别忘了,编程语言是供程序员使用的,而函数库就是程序员需要的东西。 设计优秀的函数库是很难的,并不只是写一大堆代码而已。一且函数库数量变得太多,找到一个你需要的函数有时候还不如自己动手写来得快。函数库的设计基础与语言内核一样,都是一个小规模的正交运算符集合。函数库的使用应该符合程序员的直觉,让他可以猜得出哪个函数能满足自己的需要。效率 众所周知,好的编程语言生成的代码有较快的运行速度。但是实际上,我觉得代码的运行速度不是编程语言的设计者能够控制的。高德纳很久以前就指出,运行速度只取决于一些关键的瓶颈。而在编程实践中,许多程序员都已经注意到自己很容易搞错瓶颈到底在哪里。 所以,编程时提高代码运行速度的关键是使用好的性能分析器(profiler),而不是使用其他方法,比如精心选择一种静态类型的编程语言。为了提高运行速度,并没有必要每个函数的每个参数类型都声明清楚,你只需要在瓶颈处声明清楚参数类型就可以了。所以,更重要的是你需要能够找出瓶颈到底在什么地方。 人们在使用非常高级的语言(比如Lisp)时,经常抱怨很难知道哪个部分对性能的影响比较大。可能确实如此,如果你使用一种非常抽象的语言,这也许是无法避免的。不管怎样,我认为一个好的性能分析器会解决这个问题,虽然这方面还有很长的路要走,但是未来你可以快速知道程序每个部分的时间开销。 这个问题一部分源于沟通不畅。语言设计者喜欢提高编译器的速度,认为这是对自己技术水平的考验,而最多只把性能分析器当作一个附送给使用者的赠品。但是在现实中,一个好的性能分析器对程序的帮助可能大于编译器的作用。这里又一次反映出语言设计者与用户之间发生了脱节,前者竭尽全力想要解决的问题其实方向不甚正确。 让性能分析器自动运行可能是一个好主意。它自动告诉程序员每个部分的性能,而不是非要等到程序员手动运行后才能知道。比如,当程序员编辑源码的时候,代码编辑器能够实时用红色显示瓶颈的部分。另一个方法应该是设法显示正在运行的程序的情况,这对互联网软件尤其重要,因为服务器上有很多程序同时运行,它们都需要你密切关注。自动运行的性能分析器用图形实时显示程序运行时的内存状况,甚至可以发出声音,表示出现了问题。 出现问题时,声音是很好的提示。我们在Viaweb搞了一块很大的面板,上面有各种各样的仪表盘,用来显示服务器的状况。仪表盘的指针由微型马达驱动,每当马达旋转的时候,就会发出一阵轻微的噪音。在我的工位没法看到仪表盘,但是只要我听到声音,就能立刻知道服务器出现了问题。 性能分析器甚至有可能自动找出不合理的算法。如果将来有人发现某种形式的内存访问是不合理算法的信号,我不会感到很惊讶。如果有一个小人儿可以钻进计算机看看我们的程序是怎么运行的,他可能会变成一个忙碌又悲惨的可怜虫,就像那些为政府跑腿的小人物。我总觉得自己用处理器做了很多无用功,伹是一直没有找到能够看出程序是怎样浪费运算能力的好办法。 现在有一些语言先编译成字节码(byte code),然后再由解释器执行。这样做主要是为了让代码容易移植到不同的操作系统,伹是这也可以变成一项很有用的功能。让字节码成为语言的正式组成部分,允许程序员在瓶颈处内嵌字节码,这可能是一个不错的主意。然后,针对这部分字节码的优化也就变得可以移植了。 正如许多最终用户已经意识到的,运行速度的概念正在发生变化。随着互联网软件的兴起,越来越多的程序主要不是受限于计算机的运算速度,而是受限于I/O的速度。加快I/O速度将是很值得做的一件事。在这方面,编程语言也能起到作用,有些措施是显而易见的,比如采用简洁、快速、格式化输出的函数,还有些措施则需要深层次的结构变化,比如采用缓存和持久化对象(persistent object)。 用户关心的是反应时间(response time),但是软件的另一种效率正在变得越来越重要,那就是每个处理器能够同时支持的用户数量。未来许多有趣的应用程序都将是运行在服务器端的互联网软件,所以每台服务器能够支持的用户数量就成了软件业者的关键问题。互联网软件的资本支出就取决于这个指标。 许多年以来,大多数面向最终用户的程序都不太关心效率。软件开发者总是假设用户桌面电脑的运算能力会不断增长,所以不用刻意提高软件的效率。帕金森定律^被证明与摩尔定律一样颠扑不破。软件不断膨胀,消耗光所有可以得到的资源。这一切将随着互联网软件的出现发生改变,因为硬件和软件现在捆绑在一起供应。对于那些提供互联网软件的公司来说,每台服务器支持的用户数量最大化会对降低成本产生巨大影响。^「帕金森定律(Parkinson's Law)的一种原始表达形式是“工作总是到最后一刻才会完成”,后来引申到计算机领域就变成了“数据总是会填满所有空间”,更一般性的总结则是“对一种资源的需求总是会消耗光这种资源的所有供应”。——译者注」 在一些应用程序中,处理器的运算能力是瓶颈,那么最重要的优化对象就是软件的运行速度。但是,一般情况下内存才是瓶颈,你能够同时支持的用户数量取决于用户数据所消耗的内存。编程语言在这方面也能发挥作用,对线程的良好支持将使得所有用户共享同一个内存堆(heap)。持久化对象和语言内核级别的延迟加载(lazy loading)支持也有助于减少内存需求。时间 一种编程语言要想变得流行,最后一关就是要经受住时间的考验。没人想用一种会被淘汰的语言编程,这方面已经有很多前车之鉴了。所以,大多数黑客往往会等上几年,看看某一种新语言的势头,然后才真正考虑使用它。 新事物的发明者通常对这个发现很震惊,他们没想到人们居然这样对待发明创造。但是,让别人相信一种新事物是需要时间的。我有一个朋友,他的客户第一次提出某种需求时,他很少理会。因为他知道人们有时候会想要自己并不真正需要的东西。为了避免浪费时间,只有当客户第三次或第四次提出同样的需求时,他才认真对待。这个时候客户可能已经很不高兴了,但是这至少保证他们提出的需求应该就是他们真正需要的东西。 大多数人接触新事物时都学会了使用类似的过滤机制。甚至有时要听到别人提起十遍以上他们才会留意。这样做完全是合理的,因为大多数的热门新商品事后被证明都是浪费时间的噱头,没多久就消失得无影无踪。虚拟现实建模语言VRML刚诞生时曾经轰动一时,但是我决定等到一两年后再去学习它,结果一两年后已经没有学习的必要了,因为市场已经把它遗忘了。 所以,发明新事物的人必须有耐心,要常年累月不断地做市场推广,直到人们开始接受这种发明。我们就耗费了好几年才使得客户明白ViaWeb不需要下载安装就能使用。不过,好消息是,简单重复同一个信息就能解决这个问题。你只需要不停地重复同一句话,最终人们将会开始倾听。人们真正注意到你的时候,不是第一眼看到你站在那里,而是发现过了这么久你居然还在那里。 新事物的发展改进一般也需要很长时间。大多数技术在诞生后都逐渐发生了巨大的变化,编程语言更是如此。诞生头几年,一小批早期使用者比其他因素更能促进技术发展。早期使用者都是行家,要求也很高,能够很快找出你的技术中存在的缺点。而且,如果你的用户只有很少几个人,你就能够与他们所有人保持密切接触。只要不断改进你的系统,即使给用户造成了损失,早期使用者也会对你宽容大度的。 新技术被市场接纳的方式有两种,一种是自然成长式,另一种是大爆炸式。自然成长式的一个例子就是在车库里白手起家、自力更生的创业者。几个好朋友埋头工作,在外界毫不知晓的情况下开发出某种新技术。他们把它推向市场,没有任何宣传,最初的用户寥寥无几(但是热心程度无与伦比)。创业者持续改进新技术,与此同时,通过口碑效应,用户数量不断增长。在创业者不经意间,他们已经壮大起来了。 大爆炸式的例子是有风险资本支持、在市场上大张旗鼓宣传的创业公司。他们急急忙忙地开发一个产品,推向市场的时候大肆曝光,立刻就获得了一大批使用者(至少他们希望如此)。 一般来说,车库里的创业者会妒忌大爆炸式的创业公司。后者的主导人物个个光彩照人、自信非凡,深受风险资本商的追捧。他们什么都买得起,在公关公司配合产品推出的宣传活动中,他们自己也附带成为了明星人物。自然成长式的创业者坐在自家车库里,觉得自己又穷又可怜。伹是我想他们不必难过。最终来看,自然成长式会比大爆炸式产生更好的技术,能为创始人带来更多的财富。如果你研究一下目前的主流技术,就会发现大部分都是源于自然成长式。 这种模式不仅存在于商业公司,还存在于科研活动中。Multics操作系统和Ada语言是大爆炸式项目,现在都已经销声匿迹了,而它们的继承者Unix和C语言则是自然成长式项目。再设计 著名散文家E.B.怀特说过,“最好的文字来自不停的修改”。所有优秀作家都知道这一点,它对软件开发也适用。设计一样东西,最重要的一点就是要经常“再设计”,编程尤其如此,再多的修改都不过分。 为了写出优秀软件,你必须同时具备两种互相冲突的信念。一方面,你要像初生牛犊一样,对自己的能力信心万丈;另一方面,你又要像历经沧桑的老人一样,对自己的能力抱着怀疑态度。在你的大脑中,有一个声音说“千难万险只等闲”,还有一个声音却说“早岁哪知世事艰”。 这里的难点在于你要意识到,实际上这两种信念并不矛盾。你的乐观主义和怀疑倾向分别针对两个不同的对象。你必须对解决难题的可能性保持乐观,同时对当前解法的合理性保持怀疑。 做出优秀成果的人,在做的过程中常常觉得自己做得不够好。其他人看到他们的成果觉得棒极了,而创造者本人看到的都是自己作品的缺陷。这种视角的差异并非偶然,因为只有对现状不满,才会造就杰出的成果。 如果你能平衡好希望和担忧,它们就会推动项目前进,就像自行车在保持平衡中前进一样。在创新活动的第一阶段,你不知疲倦地猛攻某个难题,自信一定能够解决它。到了第二阶段,你在清晨的寒风中看到自己已经完成的部分,清楚地意识到存在各种各样的缺陷。此时,只要你对自己的怀疑没有超过你对自己的信心,就能够坦然接受这个半成品,心想不管多难我还是可以把剩下的部分做完。 让这两股相反的力量保持平衡是很难的。初出茅庐的年轻黑客都很乐观,自以为做出了伟大的产品,从不反思和改进。上了年纪的黑客又太不自信,甚至故意回避一些挑战性很强的项目。 任何措施,只要能让“再设计”周而复始地进行下去,就都是可取的。文章可以修改到你满意为止,但是软件的修改通常来说可以无休止地进行下去。文章的读者不可能抱怨修改后新增加的内容让他们前后的思想产生了不协调,但是软件的使用者就会抱怨修改后的版本有不兼容问题。 用户是一把双刃剑。他们推动语言的发展,但也使得你不敢对语言进行大规模改造。所以,一开始的时候要精心选择用户,避免使用者过快增长。发展用户就像一种优化过程,明智的做法就是放慢速度。一般情况下,用户比较少意味着你任何时候都可以加大修改的力度。这时,对语言规格做出改变就像撕绷带,当你感到痛苦的一瞬间,痛苦就已经成为了回忆。如果用户数量庞大,修改语言带来的痛苦就将持续很长时间。 大家都知道,让一个委员会负责设计语言是非常糟糕的主意。委员会只会做出恶劣的设计。但是我觉得,委员会最大的问题在于他们妨碍了“再设计”。在委员会的主持下,修改一种语言是非常麻烦的事,没有人愿意自讨苦吃。而且,即使大多数成员不喜欢某种做法,委员会最后的决定往往还是维持现状。 就算委员会只有两个人,还是会妨碍“再设计”,典型例子就是软件内部的各个接口由不同的人负责。这时除非两个人都同意改变接口,否则接口就无法改变。因此现实中,尽管软件功能越来越强大,内部接口却往往一成不变,成为整个系统中拖后腿的部分。 一种可能的解决方法是,将软件内部的接口设计成垂直接口而不是水平接口。这意味着软件内部的模块是一个个垂直堆积起来的抽象层,层与层之间的接口完全由其中的一层控制。如果较高的一层使用了较低的一层定义的语言,那么接口就由较低的一层控制;如果较低的一层从属于较高的一层,那么接口就由较高的一层控制。梦寐以求的编程语言 让我们试着描述黑客心目中梦寐以求的语言来为以上内容做个小结。这种语言干净简练,具有最高层次的抽象和互动性,而且很容易装备,可以只用很少的代码就解决常见的问题。不管是什么程序,你真正要写的代码几乎都与你自己的特定设置有关,其他具有普遍性的问题都有现成的函数库可以调用。 这种语言的句法短到令人生疑。你输入的命令中,没有任何一个字母是多余的,甚至用到Shift键的杌会也很少。 这种语言的抽象程度很高,使得你可以快速写出一个程序的原型。然后,等到你开始优化的时候,它还提供一个真正出色的性能分析器,告诉你应该重点关注什么地方。你能让多重循环快得难以置信,并且在需要的地方还能直接嵌入字节码。 这种语言有大量优秀的范例可供学习,而且非常符合直觉,你只需花几分钟阅读范例就能领会应该如何使用此种语言。你偶尔才需要查阅操作手册,它本身很薄,里面关于限定条件和例外情况的警告寥寥无几。这种语言的内核很小,但很强大。各个函数库高度独立,而且和内核一样经过精心设计,它们都能很好地协同工作。语言的每个部分就像精密照相机的各种零件一样完美契合,不需要为了兼容性问题放弃或者保留某些功能。所有函数库的源码都很容易得到。这种语言能够很轻松地与操作系统和用其他语言开发的应用裎序对话。 这种语言以层的方式构建。较高的抽象层透明地构建在较低的抽象层之上。如果需要的话,你可以直接使用较低的抽象层。 除了一些绝对必要隐藏的东西,这种语言的所有细节对使用者都是透明的。它提供的抽象能力只是为了方便你的开发,而不是为了强迫你按照它的方式行事。事实上,它鼓励你参与它的设计,给你提供与语言创造者平等的权力。你能够对它的任何部分加以改变,甚至包括它的语法。它尽可能让你自己定义的部分与它本身定义的部分处于同等地位。这种梦幻般的编程语言不仅开放源码,更开放自身的设计。15.设计与研究 外国游客常常惊讶地发现,美国人交谈的时候,一开始总是问“你干什么工作”。我一直讨厌回答这个问题,因为一句话说不清楚。不过我最终找到了解决方法,现在如果有人问我干什么工作,我会正规对方的双眼说:“我正在设计一种Lisp语言的新方言。”如果你也有同样困扰,我推荐你也如此回答。对方就立刻转向其他话题了。 我确实是在“设计”一种编程语言,而且我不认为自己在做“研究”。我所做的工作与其他人设计一幢大楼、一把椅子、一种新字体并没有本质不同。我的目的不是发现一种“新”东西,而是做出一种很“好”的编程语言。 设计与研究的区别看来就在于,前者追求“好”(good),后者追求“新”(new)。优秀的设计不一定很“新”,但必须是“好”的;优秀的研究不一定很“好”,但必须是“新”的。我认为这两条道路最后会发生交叉:只有应用“新”的创意和理论,才会诞生超越前人的最佳设计;只有解决那些值得解决的难题(也就是“好”的难题),才会诞生最佳研究。所以,最终来说,设计和研究都通向同一个地方,只是前进的路线不同罢了。 如果把创造一种编程语言看成是设计问题,而不是科研方向,那么有何不同? 最大的不同在于你会更多地考虑用户。设计的时候,一开始总是问:我为谁设计?他们需要什么?比如,优秀的建筑师不会先设计,然后强迫用户接受,而是先研究最终用户的需求,然后做出用户需要的设计。 注意,我说的是“用户需要的设计”,而不是“用户要求的设计”。我不想让读者产生一种印象,认为设计师就像厨师一样,顾客点什么菜就一模一样做出来。艺术的各个领域有着巨大的差别,但是我觉得任何一个领域的最佳作品都不可能由对用户言听计从的人做出来。 有一句话说“顾客永远是对的”,这是指评价优秀设计的标准是看它能够多大程度上满足用户的需求。如果你的小说没人爱看,或者你做的椅子极不舒服,那么就说明你的作品失败了,被一票否决了。就算你的小说(或者椅子)有着最先进的理论指导也无济于事。 可是,让用户满意并不等于迎合用户的一切要求。用户不了解所有可能的选择,也经常弄错自己真正想要的东西。做一个好的设计师就像做一个好医生一样。你不能头痛医头,脚痛医脚。病人告诉你症状,你必须找出他生病的真正原因,然后针对病因进行治疗。 大多数优秀设计都是这样产生的,它们关注用户,并且以用户为中心。 我说设计必须考虑用户的需求,这里的“用户”并不是指所有普罗大众。事实上,你可以选择任何想要的目标用户。比如,假定你正在设计一种工具,你可以把目标用户定为初学者,也可以定为专家级用户。一种人眼里的优秀设计可能在另一种人眼里却是糟糕无比。这里的重点是你必须选出某些人作为你的目标用户。我觉得,除非设定目标用户,否则一种设计的好坏根本无从谈起。 如果目标用户群体涵盖了设计师本人,那么最有可能诞生优秀设计。如果目标用户与你本人差别很大,你往往会假定目标用户的需求比你本人的需求更简单,而不是更复杂。低估用户(即使出于善意)一般来说总是会让设计师出错。我怀疑那些设计“公共住宅项目”(housing project)^的建筑师根本没想过自己住在里面会是什么感觉。编程语言也有这种现象。C、Lisp和Smalltalk都是设计者为了自己使用而设计的,而Cobol、Ada和Java则是为了给别人使用而设计的。^「“公共住宅”指的是由政府出资建造的房产,用来出租给低收入家庭居住,类似于廉租房。——译者注」 如果你觉得自己在为傻瓜设计产品,那么很可能不仅无法设计出优秀产品,而且就连傻瓜也不喜欢你的设计。 不过,就算你的设计针对的是最高端的用户,你也一样是设计给人类使用。研究就不一样了。做数学研究时,你不会只为了方便读者理解而故意选择一种更麻烦的证明方式,你只会选择最直接、最简洁的证明。我想,一般来说科学研究都是这样。科学观点不需要服从人类工程学(ergonomic)。 到了艺术领域,情况就完全变了。设计必须以人为本。设计椅子的时候,你不能只考虑椅子,还必须考虑人体各种千奇百怪的特点,不可能回避掉这一点。所有的艺术都必须迎合人类的兴趣和极限。举例来说,不考虑其他因素时,肖像画就是比风景画更能引发观众的兴趣。文艺复兴时期的经典绘画作品都是画人的,这并非巧合。如果绘画艺术不能用来表现人类本身,那么绘画也不会成为今天这样受推崇的艺术形式了。 不管你喜不喜欢,编程语言也是以人为本的。我怀疑人类的大脑与躯干一样,都有着许多令人琢磨不透的特点。否则为什么有些事情人类特别擅长,而另一些事情人类干起来特别困难。比如,人类似乎不善于处理精细的工作,所以最好还是交给计算机处理。另一方面,如果人类真的擅长和细节打交道,那么我们应该都用机器语言编程才对。 另外,还要记住一点。怎么理解编程语言?你不要把它看成那些已完成的程序的表达方式,而应该把它理解成促进程序从无到有的一种媒介。这里的意思是说,成品的材料和开发时用的材料其实是不一样的。搞艺术的人都知道,这两个阶段往往需要不同的媒介。比如,大理石是一种非常良好、耐用的材料,很适合用于最后的成品,但是它极其缺乏弹性和灵活性,所以不适合在构思阶段用来做模型。 最后写出来的程序就像已经完成的数学证明一样,是一棵经过精心修剪的树木,上面杂乱滋生的树杈都已经被剪去了。所以,评价一种语言的优劣不能简单地看最后的程序是否表达得很漂亮,而要看程序从无到有的那条完成路径是否很漂亮。某种设计使得最后的程序非常漂亮,伹是不一定同时具备漂亮的编程过程。比如,我写过一些宏,它们的作用是自动生成另一些宏,它们看上去非常精美优雅,就像一粒粒精细的宝石。但是,开发过程非常丑陋,我就是连续好几个小时不停地试错,而且老实说,至今仍然无法完全确定它们是否百分之百正确。 我们常常采用错误的方法评价编程语言,只看一眼最后完成的程序就做出判断。同一个软件有两种不同语言开发的版本,你发现其中一个版本比另一个版本短得多,于是非常自信地认定前者的编程语言比后者的更好。但是,如果你从艺术创作的角度思考这个问题,就不太可能这样评价编程语言。因为你不想最后只剩下一种像大理石那样漂亮、又像大理石那样难用的编程语言。 比如,开发软件的时候,一个“交互式顶层解释器”(interactive toplevel)会带来巨大的优势。在Lisp语言中,这种解释器就叫做“读取-求值-打印”循环(read-eval-print loop)。有了这个解释器后,语言的设计就会受到巨大影响。静态类型语言不适合部署这样的解释器,因为静态类型语言要求在使用变量前先声明类型,这对于“交互式顶层解释器”行不通。当你在解释器中输入表达式,然后对变量x进行赋值,接着再对x做进一步处理时,你只想尽快看到结果,肯定不想很麻烦地先声明jc的类型。你也许不同意“交互式顶层解释器”为软件开发带来便利的说法,但是如果你接受它,同意易于使用的编程语言必须有一个这样的解释器,那么强制声明变量类型的做法就是与这个解释器不兼容,因此结论就是所有的静态类型语言都不易于编程。 为了做出优秀的设计,你必须贴近用户,始终寸步不离,永远站在用户的角度调整自己的构想。19世纪英国作家简·奧斯汀的小说为何如此出色?一个原因就是她把自己的作品大声读给家人听,所以她就不会陷入孤芳自赏难以自拔的境地,不会长篇累牍地赞叹自然风光,也不会滔滔不绝地宣扬自己的人生哲学。(事实上,简·奥斯汀还是在小说里宣扬了自己的人生哲学,不过她把它编进故事之中,而不是直接像贴标签那样讲出来。)你可以随便找一本平庸的“文学”读物,想象一下把它当作自己的作品读给朋友们听,这样会让你真切地感受到那些“文学”读物高高在上的视角,读者必须承受所有沉重的负担才能阅读这些作品。 在软件领域,贴近用户的设计思想被归纳为“弱即是强”(Worse is Better)模式^。这个模式实际上包含了好几种不同的思想,所以至今人们还在争论它是否真的成立。但是,其中有一点是正确的,那就是如果你正在设计某种新东西,就应该尽快拿出原型,听取用户的意见。^「“弱即是强"指的是一种软件传播的模式,由Common Lisp专家理査德·加布里埃尔(Richard P. Gabriel)于1991年在Lisp: Good News, Bad News, How to Win Big(m/WIB.html)一文中首先提出。它的含义非常广泛,涉及软件设计思想的各个方面,其中的一个重要结论就是软件功能的增加并不必然带来质量的提高。有时候,更少的功能(“弱”)反而是更好的选择(“强”),因为这会使得软件的可用性提高。相比那些体积庞大、功能全面、较难上手的软件,一种功能有限但易于使用的软件可能对用户有更大的吸引力。加布里埃尔本人经常举Unix和C语言的例子,Unix和C在设计上考虑了实际环境,放弃了一些功能,但是保证了简单性,这使得它们最终在竞争中胜出,成为主流操作系统和编程语言。——译者注」 与之对照,还有另一种软件设计思想,也许可以被称为“万福玛丽亚”模式。它不要求尽快拿出原型,然后再逐步优化,它的观点是你应该等到完整的成品出来以后再一下子隆重地推向市场,就像圣母玛丽亚降临一样,哪怕整个过程漫长得像橄揽球运动员长途奔袭、达阵得分也没有关系。在互联网泡沫时期,无数创业公司因为相信了这种模式而自毁前程。我还没听说过有人采用这种模式而获得成功。 软件领域以外的人可能没听过“弱即是强”,所以意识不到这种模式在艺术领域普遍存在。以绘画为例,文艺复兴时期就有人发现了这一点。如今,几乎所有的美术老师都会告诉你准确画出一个事物的方法,不是沿着轮廓慢慢一个部分、一个部分地把它画出来,因为这样的话各个部分的错误会累积起来,最终导致整幅画失真。你真正应该采用的方法是快速地用几根线画出一个大致准确的轮廓,然后再逐步地加工草稿。 在大多数艺术领域,原型使用的材料与成品的材料一般来说是不一样的。印刷活字先画在纸上,然后才做成铅字。雕塑先用石蜡创作,然后才用青铜浇铸。地毯图案先用墨水画出纸型,然后才织成地毯。建筑物先做出木模型,然后才做成石头建筑。 为什么15世纪油画首次亮相会引起轰动并很快流行起来?原因就是油彩使得画家可以在原型上直接画出最后的样子。你可以按照自己的想法画出初稿,但是它并不对你构成限制。接下来你可以逐步加上细节,甚至对初稿做出重大修改,直到最后完成。 软件开发也可以这样做。原型(prototype)并不只是模型(model),不等于将来一定要另起炉灶,你完全能够在原型的基础上直接做出最后的成品。我认为,只要有可能,你就应该这样做。这样的方式使得你可以利用在开发过程中一路产生的新想法。不过更重要的是,这样做有助于鼓舞士气。 士气是设计的关键因素。令我吃惊的是,大家很少提到这一点。我的一位美术启蒙老师告诉我:如果你觉得画某样东西很乏味,那么你画出来的东西就会真的很乏味。比如,假设你必须画一幢建筑物,你决定从每一块砖头开始画起。你觉得自己可以坚持下去,但是画到一半的时候突然感到很厌倦,于是你就不再认真观察每块砖头并画出它们各自不同的特点,而是以一种机械重复的方式草草地把砖头画完了事。这样一来,你的作品效果就很差,甚至还不如一开始就不采用写实手法,只是若隐若现地暗示砖头的存在。 先做出原型,再逐步加工做出成品,这种方式有利于鼓舞士气,因为它使得你随时都可以看到工作的成效。开发软件的时候,我有一条规则:任何时候,代码都必须能够运行。如果你正在写的代码一个小时之后就可以看到运行结果,这好比让你看到不远处就是唾手可得的奖励,你因此会受到激励和鼓舞。其他艺术领域也是如此,尤其是油画。大多数画家都是先画一个草图,然后再逐步加工。如果你采用这种方式,那么从理论上说,你每天收工的时候都可以看到整体的效果,不会对最后的成品一点感觉都没有。跟你说实话吧,画家之间甚至流传着一句谚语:“画作永远没有完工的一天,你只是不再画下去而已。”这种情况对于第一线的程序员真是再熟悉不过了。 士气也可以解释为什么很难为低端用户设计出优秀产品。因为优秀设计的前提是你自己必须喜欢这种产品,否则你不可能对设计有兴趣,更不要说士气高昂了。为了把产品设计好,你必须对自己说:“哇,这个产品太棒了,我一定要设计好!”而不是心想:“这种垃圾玩意,只有傻瓜才会喜欢,随便设计一下就行了。” 设计意味着做出符合人类特点和需要的产品。但是,“人类”不仅包括用户,还包括设计师,所以设计工作本身也必须符合设计师的特点和需要。