百年语言
2003年4月
(本文源自作者在PyCon 2003上的主题演讲。)
预测一百年后的生活是什么样子是很难的。我们只能确定少数几件事。我们知道每个人都会驾驶飞车,分区规划法会放松限制以允许建造数百层高的建筑,那时候大部分时间都是黑夜,而且所有女性都会接受武术训练。在这里,我想聚焦于这幅图景中的一个细节:他们会用什么样的编程语言来编写控制那些飞车的软件?
这个问题值得思考,不仅仅是因为我们真的有机会用到这些语言,更是因为,如果我们幸运的话,我们将沿着从此刻走向彼时的路径上使用这些语言。
我认为,就像物种一样,语言会形成进化树,到处都有死胡同的分支。我们已经能看到这种情况正在发生。Cobol语言,尽管曾经风靡一时,但似乎没有任何智识上的后代。它是一个进化上的死胡同——一种尼安德特人式的语言。
我预测Java也会有类似的命运。人们有时给我发邮件说:“你怎么能说Java不会成为一种成功的语言呢?它已经是一种成功的语言了。”我承认,如果你以关于它的书籍所占的书架空间(尤其是关于它的个人专著)来衡量成功,或者以相信必须学会它才能找到工作的本科生数量来衡量,那么它确实是一种成功的语言。当我说Java不会成为一种成功的语言时,我的意思更具体:Java将会像Cobol一样,成为一个进化上的死胡同。
这只是一个猜测。我可能错了。我在这里的重点不是贬低Java,而是提出进化树的问题,让人们去追问:语言X在进化树的哪个位置?提出这个问题的原因不仅仅是为了让我们的灵魂在一百年后能说:“我早就告诉过你了。”而是因为,保持在主枝附近是寻找现在适合编程的好语言的有用启发式方法。
在任何给定的时间,你可能在进化树的主枝上最快乐。即使当时还有很多尼安德特人,成为其中一员也一定很糟糕。克罗马农人会不断过来揍你、抢你的食物。
我想知道一百年后语言会是什么样子,是为了知道现在应该押注在树的哪个分支上。
语言的进化与物种的进化不同,因为分支可以汇合。例如,Fortran分支似乎正在与Algol的后代合并。理论上这对物种也是可能的,但不太可能发生在比一个细胞更大的生物身上。
语言更容易发生汇合,部分原因是可能性的空间更小,部分原因是突变不是随机的。语言设计者会有意地吸收其他语言的思想。
对于语言设计者来说,思考编程语言的进化可能走向何方尤其有用,因为他们可以据此调整方向。在这种情况下,“留在主枝上”就不仅仅是选择一门好语言的方法,更成为做出正确语言设计决策的启发式原则。
任何编程语言都可以分为两部分:一组扮演公理角色的基本运算符,以及语言的其余部分——这部分原则上可以用这些基本运算符来编写。
我认为基本运算符是语言长期生存中最重要的因素。其余部分你可以改变。这就像买房子的规则:首先要考虑位置。其他一切你之后都可以修整,但位置你无法改变。
我认为重要的不仅在于公理要选得好,而且在于公理要少。数学家们对公理一直有这样的感觉——越少越好——我认为他们发现了重要的东西。
至少,仔细审视一种语言的核心,看看是否有可以剔除的公理,这必定是一个有用的练习。在我漫长的懒散职业生涯中,我发现杂乱会滋生杂乱,我在软件中、床底下以及房间角落里都目睹了这种情况的发生。
我有个直觉:进化树的主枝会穿过那些拥有最小、最干净核心的语言。一种语言能用自身写出的部分越多,就越好。
当然,我甚至在问一百年后编程语言会是什么样子时,就已经做了一个很大的假设。一百年后我们还会编写程序吗?难道我们不会只是告诉计算机我们想要它们做什么吗?
到目前为止,在这方面还没有取得太多进展。我的猜测是,一百年后,人们仍然会使用我们能认出的那种程序来告诉计算机做什么。可能有些任务我们现在通过编写程序来解决,而一百年后你不需要写程序就能解决,但我认为,仍然会有大量我们今天所从事的那类编程工作。
认为任何人都能预测任何一种技术在一百年后的样子,这似乎有些冒昧。但请记住,我们已经有了将近五十年的历史。当我们考虑到语言在过去五十年中进化得多么缓慢时,展望一百年是一个可以把握的想法。
语言进化缓慢,因为它们并不是真正的技术。语言是记号系统。程序是对你想要计算机为你解决的问题的形式化描述。因此,编程语言的进化速率更像是数学记号的进化速率,而不是交通或通信技术的进化速率。数学记号确实在进化,但不会像技术那样出现巨大的飞跃。
无论一百年后计算机由什么构成,似乎可以安全地预测它们会比现在快得多。如果摩尔定律继续成立,它们将会快74乘以10的18次方(73,786,976,294,838,206,464)倍。这有点难以想象。事实上,在速度方面最可能的预测也许是摩尔定律会停止工作。任何声称每十八个月翻一番的事物似乎最终都可能遇到某种根本性的限制。但我毫不怀疑计算机会变得非常非常快。即使它们最终只快了区区一百万倍,这也应该会实质性改变编程语言的基本规则。除此之外,还会有更多空间容纳那些现在被认为是慢的语言,即那些不能产生非常高效代码的语言。
然而,有些应用仍然需要速度。我们想用计算机解决的某些问题是由计算机本身创造的;例如,你必须处理视频图像的速度取决于另一台计算机生成它们的速度。还有一类问题天生具有无限吸收计算周期的能力:图像渲染、密码学、模拟。
如果一些应用可以越来越低效,而另一些应用则继续要求硬件能提供的所有速度,那么更快的计算机将意味着语言必须覆盖越来越宽的效率范围。我们已经看到这种情况在发生。以过去几十年的标准来看,当前一些流行的新语言的实现浪费得惊人。
这不仅仅是编程语言领域发生的事情。这是一个普遍的历史趋势。随着技术的进步,每一代人都能做上一代人认为浪费的事情。三十年前的人会惊讶于我们如此随意地打长途电话。一百年前的人会更加惊讶,有一天一个包裹会从波士顿经由孟菲斯运到纽约。
我已经可以告诉你,在接下来的一百年里,更快的硬件将给我们带来的那些额外计算周期会发生什么。它们几乎全部都会被浪费掉。
我是在计算机能力稀缺时学会编程的。我记得为了把Basic程序塞进4K TRS-80的内存,我把程序里的所有空格都去掉了。想到所有这些极其低效的软件在无休止地重复做着相同的事情来消耗计算周期,我觉得有点恶心。但我认为我的这种直觉是错误的。我就像一个在贫穷中长大的人,即使是为了像看医生这样重要的事情,也舍不得花钱。
有些浪费确实令人厌恶。例如,SUV即使使用永远不会耗尽且不产生污染的燃料,也可以说是令人厌恶的。SUV令人厌恶,因为它们是解决一个令人厌恶的问题的方案。(如何让小型货车看起来更阳刚。)但并非所有浪费都是坏的。现在我们有基础设施支持了,计算长途电话的分钟数开始显得斤斤计较。如果你有资源,把所有电话都看作一种类型的事情,无论对方在哪里,都更优雅。
有好的浪费,也有坏的浪费。我感兴趣的是好的浪费——那种通过花费更多,我们能获得更简单设计的浪费。我们将如何利用从新的、更快的硬件中获得的可浪费计算周期的机会呢?
我们这些拥有弱小计算机的人,对速度的渴望如此根深蒂固,以至于需要有意识地去克服它。在语言设计中,我们应该有意识地寻找那些可以用效率换取哪怕是最轻微便利性的场合。
大多数数据结构的存在都是为了速度。例如,今天许多语言既有字符串又有列表。从语义上讲,字符串或多或少是列表的子集,其中元素是字符。那么,为什么你需要一个单独的数据类型呢?实际上你并不需要。字符串仅仅是为了效率而存在。但是,为了加速程序运行而用各种权宜之计来弄乱语言的语义,这很糟糕。在语言中拥有字符串似乎是一种过早优化的案例。
如果我们把语言的核心看作一组公理,那么仅仅为了效率而添加不增加任何表达能力的额外公理,肯定是糟糕的。效率很重要,但我认为那不是获得效率的正确方式。
我认为解决这个问题的正确方法是将程序的含义与实现细节分开。不是同时拥有列表和字符串,而是只拥有列表,并提供某种方式给编译器优化建议,允许它在必要时将字符串布局为连续的字节。
由于在程序的大部分中速度并不重要,你通常不需要费心进行这种微观管理。随着计算机变得更快,这将会越来越成为事实。
少说实现细节也应该使程序更加灵活。在编写程序的过程中,规范会发生变化,这不仅是不可避免的,而且是可取的。
“随笔”这个词来自法语动词“essayer”,意思是“尝试”。一篇随笔,在其原始意义上,是你为了试图弄清某件事而写的东西。这在软件领域也会发生。我认为一些最好的程序就是随笔,在某种意义上,作者在开始写作时并不确切知道他们想要写什么。
Lisp黑客们已经知道数据结构灵活性的价值。我们倾向于编写程序的第一个版本,让它用列表来做所有事情。这些初始版本可能低效得惊人,以至于需要有意识的努力才能不去想它们在做什么,就像对我来说,吃牛排需要有意识的努力不去想它的来源一样。
一百年后的程序员最想要的是什么?最主要的是,一种能让你以最小的努力,拼凑出某个程序效率低得惊人的一号版本的语言。至少,用我们今天的话来说是这样。而他们会说,他们想要的是一种容易编程的语言。
低效的软件并不可耻。可耻的是一种让程序员做无谓工作的语言。浪费程序员的时间才是真正的高效问题,而非浪费机器时间。随着计算机速度越来越快,这一点将愈发清晰。
我认为,摆脱字符串已经是我们敢于思考的事了。我们在 Arc 中做到了这一点,看起来是个胜利;有些用正则表达式难以描述的操作,用递归函数却能轻松实现。
这种数据结构的扁平化会走多远?我能想到一些连我自己(尽管我刻意拓宽思路)都感到震惊的可能性。比如,我们会摒弃数组吗?毕竟,它们不过是哈希表的子集,其中键是整数向量。我们会用列表替换哈希表本身吗?
还有比这更令人震惊的前景。例如,麦卡锡 1960 年描述的 Lisp 语言并没有数字。逻辑上,你不需要单独的数字概念,因为你可以用列表表示它们:整数 n 可以表示为一个包含 n 个元素的列表。这样也能做数学运算。只是效率低得令人难以忍受。
实际上,没有人真正提出过用列表来实现数字。事实上,麦卡锡 1960 年的论文在当时根本无意被实现。那是一项理论练习,试图创造一种比图灵机更优雅的替代品。当有人意外地将这篇论文转化为一个可运行的 Lisp 解释器时,数字肯定不是用列表表示的;它们用二进制表示,就像其他所有语言一样。
编程语言能否走得如此之远,以至于基本数据类型中都没有数字了?我问这个问题,与其说是一个严肃的问题,不如说是与未来玩一场“斗鸡”游戏。这就像假设中的不可抗拒之力遇上坚不可摧之物的情形——这里,是难以想象的低效实现遇上难以想象的巨大资源。我看不出有什么不可以的。未来相当漫长。如果有什么能减少核心语言中公理的数量,那似乎在时间趋于无穷时是值得押注的一方。如果这个想法在一百年后仍然难以接受,也许一千年后就不会了。
需要澄清的是,我并不是建议所有数值计算实际上都用列表来执行。我建议的是,在任何关于实现的附加标注之前,核心语言就这样定义。实际上,任何想进行一定量数学运算的程序可能都会用二进制表示数字,但这将是优化,而不是核心语言语义的一部分。
另一种消耗周期的方式是在应用程序和硬件之间设置多层软件。这也是我们已经看到的趋势:许多新语言都被编译成字节码。比尔·伍兹曾告诉我,根据经验法则,每一层解释都会带来 10 倍的速度开销。这额外的成本换来的是灵活性。
Arc 的第一个版本就是这种多层缓慢的极端情况,但也有相应的好处。它是一个经典的“元循环”解释器,构建在 Common Lisp 之上,与麦卡锡原始 Lisp 论文中定义的 eval 函数有着明显的家族相似性。整个程序只有几百行代码,因此很容易理解和修改。我们使用的 Common Lisp——CLisp——本身运行在字节码解释器之上。所以这里我们有两层解释,其中一层(最上层)效率低得惊人,而语言仍然是可用的。勉强可用,我承认,但可用。
将软件写成多层结构即使是在应用程序内部也是一种强大的技术。自底向上的编程意味着将程序写成一系列层,每一层都作为上一层的语言。这种方法往往会产生更小、更灵活的程序。它也是通往那个圣杯——可复用性——的最佳途径。语言从定义上就是可复用的。你的应用程序中越多部分能被下推成用于编写该类型应用程序的语言,你的软件就会有更多部分是可复用的。
不知何故,20 世纪 80 年代可复用的概念与面向对象编程联系在一起,而且无论多少相反的证据似乎都无法将其摆脱。但是,尽管一些面向对象的软件是可复用的,但使其可复用的是它的自底向上性,而不是它的面向对象性。想想库:它们可复用是因为它们就是语言,无论它们是否以面向对象风格编写。
顺便说一句,我并不预言面向对象编程的消亡。虽然我认为它对优秀的程序员没有太多贡献,除了在某些特定领域,但它对大型组织来说是不可抗拒的。面向对象编程提供了一种可持续的方式编写意大利面代码。它让你以一系列补丁的方式累积程序。大型组织总是倾向于以这种方式开发软件,我预计一百年后这种情况会与今天一样。
既然我们在谈论未来,我们最好谈谈并行计算,因为这个想法似乎就在这里。也就是说,无论你何时谈论,并行计算似乎总是即将发生的事情。
未来会赶上它吗?人们谈论并行计算即将到来至少已经 20 年了,而它至今对编程实践影响不大。或者还没有影响?芯片设计师已经不得不考虑它了,那些试图在多 CPU 计算机上编写系统软件的人也必须如此。
真正的问题是,并行性会在抽象阶梯上走多远?一百年后,它会影响甚至应用程序程序员吗?还是说它会成为编译器编写者考虑的事情,但在应用程序的源代码中通常不可见?
有一点似乎很可能:大多数并行机会将被浪费。这是我更普遍的预测的一个特例,即我们获得的大部分额外计算能力都将被浪费。我预期,就像底层硬件惊人的速度一样,并行性将是如果你明确要求就能获得的东西,但平常不会使用。这意味着我们在一百年后拥有的并行性,除非在特殊应用中,否则不会是大规模并行。我预期对普通程序员来说,它更像是一种能派生并行运行的进程的能力。
而这将,就像要求特定的数据结构实现一样,是你在程序生命周期相当晚的时候才会做的事情,当你试图优化它时。一号版本通常不会利用并行计算带来的任何优势,就像它们也忽视了特定数据表示带来的优势一样。
除了特殊类型的应用,并行性不会渗透到一百年后编写的程序中。如果它这样做了,那就成了过早优化。
一百年后会有多少种编程语言?最近似乎有大量新的编程语言出现。部分原因是更快的硬件允许程序员根据应用在速度和便利性之间做出不同的权衡。如果这是一个真正的趋势,我们一百年后拥有的硬件应该只会加剧这种情况。
然而,一百年后可能只有少数几种广泛使用的语言。我这么说部分原因是乐观:似乎如果你真的做得好,你可以创造一种语言,它既适合编写慢速的一号版本,又能在必要时通过给编译器正确的优化建议而生成非常快的代码。所以,既然我很乐观,我就要预测,尽管可接受效率和最高效率之间会有巨大差距,一百年后的程序员会拥有能够覆盖其中大部分差距的语言。
随着这个差距拉大,性能分析器将变得越来越重要。目前人们对性能分析关注不多。许多人似乎仍然相信,获得快速应用程序的方法是编写生成快速代码的编译器。随着可接受性能和最大性能之间的差距拉大,将越来越清楚的是,获得快速应用程序的方法是有一个好的指导从一端到另一端。
当我说可能只有少数几种语言时,我并不是指特定领域的“小语言”。我认为这种嵌入式语言是一个伟大的想法,我预期它们会大量涌现。但我预期它们会被写得足够薄,让用户能看到底下的通用语言。
未来谁将设计语言?过去十年中最令人兴奋的趋势之一是像 Perl、Python 和 Ruby 这样的开源语言的兴起。语言设计正被黑客们接管。迄今为止的结果是混乱的,但令人鼓舞。例如,Perl 中有一些令人惊艳的新奇想法。许多是糟糕透顶的,但雄心勃勃的努力总是如此。按照目前的变异速度,天知道 Perl 一百年后会进化成什么样子。
“会者不教”的说法并不正确(我认识的一些最优秀的黑客就是教授),但那些教书的人确实有很多事情做不了,这是事实。研究施加了限制性的种姓制度。在任何学术领域,有些课题是可以研究的,有些则不可以。不幸的是,可接受和禁止的课题之间的区别通常基于在研究论文中描述时听起来有多学术化,而不是对于获得好结果有多重要。极端的例子可能是文学;研究文学的人很少说出对从事文学创作的人有丝毫用处的话。
虽然科学领域的情况稍好一些,但允许做的工作与能够产生好语言的工作之间的重叠小得令人不安。(奥林·希弗斯曾对此发出雄辩的抱怨。)例如,类型似乎是研究论文取之不尽的源泉,尽管静态类型似乎排除了真正的宏——而没有宏,在我看来,没有任何语言值得使用。
这种趋势不仅在于语言正作为开源项目而非“研究”来开发,还在于语言正由需要使用它们的应用程序程序员而非编译器编写者设计。这似乎是一个好的趋势,我预期它会继续下去。
与一百年后的物理学几乎必然无法预测不同,我认为原则上现在就有可能设计出一种能吸引一百年后用户的语言。
设计语言的一种方式是直接写下你希望能够编写的程序,不管是否有编译器能翻译它或硬件能运行它。当你这样做时,你可以假设资源是无限的。似乎我们今天也应该能够像一百年后一样想象无限资源。
一个人会想编写什么样的程序?当然是最省事的程序。
但又不完全是:如果编程观念尚未被当前使用的语言所影响,那么最省事的程序才算数。这种影响无处不在,以至于要克服它需要付出巨大努力。你可能会想,像我们这样懒惰的生物,应该很容易看出如何用最少的力气来表达一个程序。事实上,我们对“可能之事”的想法往往被我们思考所用的语言所局限,以至于更简便的程序表达方式反而显得非常惊人。它们是需要你去发现的东西,而不是自然而然就能想到的。
这里有一个有用的技巧:用程序的长度来近似衡量编写它的工作量。当然不是指字符长度,而是指不同语法元素的数量——基本上就是解析树的大小。最短的程序不一定完全等同于最省事的程序,但已经足够接近,与其瞄准模糊的“最省事”这个目标,不如瞄准明确的“简洁”这个目标。于是语言设计的算法就变成了:看一个程序,问自己,有没有更短的写法?
在实践中,用一门想象中的“百年语言”来编写程序,其效果会因你离核心的远近而有所不同。排序例程你现在就能写。但要预测一百年后需要什么样的库,恐怕很难。很可能许多库将用于如今尚不存在的领域。比如,如果SETI@home成功了,我们就需要与外星人通信的库。当然,除非他们已经足够先进,以至于已经在用XML通信了。
而在另一个极端,我认为今天你就可能设计出核心语言。事实上,有些人可能会说,它在1958年就已经基本设计好了。
如果百年语言今天就能用,我们会愿意用它编程吗?回答这个问题的一种方式是回顾过去。如果现在的编程语言在1960年就存在了,会有人愿意使用它们吗?
在某些方面,答案是否定的。今天的语言预设了一些1960年尚不存在的基础设施。例如,像Python这样依赖缩进的语言,在打印机终端上不会很好用。但抛开这类问题——比如假设所有程序都只是写在纸上——1960年的程序员会喜欢用我们现在使用的语言来编写程序吗?
我认为会的。
一些想象力较差的程序员,他们把早期语言的痕迹融入了自己对程序的概念中,可能会遇到困难。(不进行指针运算怎么操作数据?不用goto怎么实现流程图?)但我认为,最聪明的程序员,如果拥有今天的语言,他们会毫无困难地充分利用它们。
如果我们现在就有百年语言,它至少能成为一种极好的伪代码。用它来编写软件又如何?由于百年语言需要为某些应用生成快速的代码,它想必能生成足够高效的代码,在我们的硬件上运行得足够好。我们可能比一百年后的用户需要给出更多的优化建议,但这仍然可能是净收益。
现在,如果把这两个想法结合起来,就会产生有趣的可能性:(1) 百年语言原则上今天就可以设计出来;(2) 如果这样的语言存在,今天用它来编程可能不错。当你这样摆出这些想法时,很难不想:为什么不现在就开始编写百年语言呢?
当你从事语言设计时,我认为拥有这样一个目标并时刻牢记在心是件好事。学开车时,教练教你的一个原则是:对齐车身时,不要用引擎盖去对准路面上的标线,而是要瞄准远处的某个点。即使你只关心接下来十英尺内发生什么,这也是正确的做法。我认为我们在编程语言方面也能且应该这样做。
注释
我相信Lisp Machine Lisp是第一门体现这一原则的语言:声明(除动态变量的声明外)仅仅是优化建议,而不会改变正确程序的含义。Common Lisp似乎是第一门明确陈述这一点的语言。