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