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