Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

百年之后的编程语言

原文:The Hundred-Year Language
作者:Paul Graham 发表:2003-04
译者:Claude(baoyu-translate)

2003 年 4 月

(本文由 PyCon 2003 主题演讲改写。)

预测一百年后的生活很难。我们能笃定的事情不多。我们知道每个人都会开飞行汽车,区划法(城市规划法规)会被放宽到允许建上百层的高楼,大多数时候天会黑着,所有女性都会练就一身武功。本文我想把镜头对准这幅图景里的一个细节——他们到时候会用什么样的编程语言来写控制那些飞行汽车的软件?

值得想一下,倒不是因为我们真有机会用上那时候的语言;而是——如果走运——我们将会用上从今天到那时这条路径上的某种语言。

我认为,跟物种一样,语言会形成进化树,沿途到处分叉出“死胡同“。这一点我们今天已经看得到。Cobol 一度颇为流行,但似乎没有什么思想上的后裔——它是一条进化死胡同,一种“尼安德特人“语言。

我预测 Java 会有同样的命运。有人写信给我:“你怎么能说 Java 不会成为一门成功的语言?它本来就已经是一门成功的语言了。”——好吧,如果你用“书架上关于它的书占多大地方“(尤其是关于它的单本书),或者“多少本科生认为必须学它才能找到工作“来衡量成功,那我承认它是。但我说“Java 不会成为一门成功的语言“时,意思更具体:Java 会和 Cobol 一样,最终成为一条进化死胡同。

这只是猜测,我可能错。我这里的目的不是怼 Java,而是把“进化树“这件事提出来,让人们去问:语言 X 在树上的哪个位置?问这个问题,不只是为了让我们的鬼魂在一百年后说一句“我早跟你说了“,更是因为——靠近主干,是寻找“今天写起来就好的语言“的一条有用启发式。

任意时间点上,你大概都最好待在进化树的主干上。即便在尼安德特人还很多的年代,做一个尼安德特人想必也很糟——克罗马农人(晚期智人)会不停跑过来揍你、抢你食物。

我之所以想知道一百年后的语言长什么样,是想知道今天该把宝押在哪条树枝上。

语言的进化和物种的进化不一样,因为它的分支可以汇合。比如 Fortran 这一支,看起来正在和 Algol 的后裔合流。理论上物种也能这样,但比单细胞大的生物身上似乎还没发生过。

语言更容易出现汇合,部分原因是它们的可能性空间更小,部分原因是它们的“突变“不是随机的——语言设计者会有意吸收别的语言的想法。

对语言设计者来说,琢磨“编程语言的进化大概会通向何方“格外有用——因为他们可以据此调整方向。这种情况下,“待在主干上“就不只是挑一门好语言的办法,而成为做出正确语言设计决策的启发式。

任何编程语言都可以分成两部分:一组扮演“公理“角色的基本算子,加上语言的其余部分——而其余部分原则上可以用这些基本算子写出来。

我认为基本算子是决定一门语言长期生存的最重要因素。其余部分都还能改。这就像买房子时的那条规则:地段第一位。其他都可以以后修,唯独地段你修不了。

我认为,公理不仅要选得好,数量还要少。数学家从来都这么看待公理——越少越好——我觉得他们是对的。

至少,仔细审视一门语言的“内核“、看看有没有能淘汰掉的公理,本身就是一项有用的练习。我作为一个常年邋遢的人发现:杂物会催生更多杂物——这件事在床底下、屋角里我见过,在软件里我也见过。

我有一种直觉:进化树的主干,会穿过那些拥有最小、最干净内核的语言。一门语言里能用它自己写出的部分越多,它就越好。

当然,光是问“一百年后的编程语言长什么样“,我已经在做一个很大的假设了。一百年后我们还在写程序吗?是不是直接告诉电脑我们想要它做什么就行了?

到目前为止,这个方向上没什么大进展。我猜,一百年后,人们仍然会用我们今天还能认出来是程序的东西去告诉电脑做事。也许有些今天靠写程序解决的问题,到那时候不再需要程序了;但仍会有相当一部分编程,是我们今天这种意义上的编程。

预测任何一项技术一百年后的样子,听起来很狂妄。但记住,我们身后已经有差不多五十年的历史了。考虑到过去五十年里语言演化得多么慢,往前看一百年,其实是个可以把握的尺度。

语言演化得慢,是因为它们其实不是技术,而是记号系统。一段程序是你想让电脑替你解决的问题的形式化描述。所以编程语言的演化速率,更像数学记号的演化速率,而不像交通或通讯的演化速率。数学记号也演化,但不会有技术那种巨人步伐式的跃迁。

不管一百年后的电脑是用什么造的,可以稳妥预测的是:它们会比现在快得多。如果摩尔定律继续兑现,它们会快上 74 京(73,786,976,294,838,206,464;京 = 10^16)倍。这有点难想象。事实上,关于“速度“最可能的预测,恐怕反倒是“摩尔定律会失效“——任何号称每 18 个月翻一倍的东西,最终都会撞上某种基本极限。但我相信电脑会快得多——一点不困难。哪怕只快了区区一百万倍,编程语言的游戏规则也会被实质性地改写。除别的之外,会给“按今天标准算很慢的语言“——也就是不能产出非常高效代码的语言——留出更多空间。

但仍然会有一些应用要求速度。我们想用电脑解决的一些问题本身就是电脑制造出来的——比如,你需要以多快的速率处理视频,取决于另一台电脑能以多快的速率生成它们。还有一类问题天生有无穷的“算力吸收“能力:图像渲染、密码学、模拟。

如果一些应用可以越来越低效,而另一些仍要榨干硬件的全部速度——那么“电脑变快“就意味着语言要覆盖越来越宽的“效率谱“。这一幕已经在上演。今天某些流行新语言的实现,按几十年前的标准看,浪费得令人震惊。

这件事不只发生在编程语言上,是一种普遍的历史趋势。技术每提升一档,下一代人就能做出上一代人会觉得“浪费“的事。三十年前的人会震惊于我们打长途电话的随意。一百年前的人则会更震惊:一个包裹竟然会从波士顿出发、经孟菲斯转运、最后到纽约。

未来一百年里,更快的硬件会给我们的那些额外算力,我现在就能告诉你:它们几乎都会被浪费掉。

我学编程是在算力稀缺的年代。我还记得为了把 Basic 程序塞进 4K 内存的 TRS-80(1977 年的早期家用电脑),我把所有空格都删掉。一想到今天那么多惊人地低效的软件正一遍又一遍地烧掉算力做同一件事,我就有点反胃。但我觉得,我这种直觉是错的——我就像一个穷出身的人,连看医生这种重要的事都舍不得花钱。

有些浪费确实让人作呕。SUV 就是个例子——哪怕它烧的是永远用不完且零污染的燃料,可以说也照样令人厌恶。SUV 之所以令人厌恶,是因为它是给一个令人厌恶的问题准备的解:怎么把 minivan 弄得更“男人味“一点。但不是所有浪费都是坏的。今天既然有了支撑长途电话的基础设施,再去一分一秒地数你的长途时长,反倒显得小家子气了。如果你有资源,更优雅的做法是把所有电话当成同一类东西——不管对方在哪儿。

浪费分两种:好浪费和坏浪费。我感兴趣的是好浪费——那种“花得多一些,换来更简单的设计“。我们会怎么利用新的、更快的硬件给我们的“挥霍算力“的机会?

对速度的渴求在我们身上扎得太深——毕竟我们用的电脑还很弱小——要克服它得有意识地用力。在语言设计中,我们应当主动去寻找那种“为了便利哪怕只多一点点,就愿意牺牲效率“的场景。

大多数数据结构是为速度而存在的。比如,今天许多语言里同时有字符串和列表。从语义上看,字符串大致就是“元素是字符的列表“——是列表的一个子集。那为什么需要一种独立的数据类型?其实并不需要。字符串只是为了效率而存在。但用一些“让程序跑得快“的小把戏去把语言的语义弄乱,是很挫的。在语言里加上字符串这件事,本身像是过早优化的一个例子。

如果我们把语言的内核看成一组公理,那么——纯粹为了效率,加进既不增加表达力、又是多余的公理——这听起来确实让人不舒服。效率重要,但我不觉得这是获得效率的正确方式。

我认为正确的解法是:把程序的语义和实现细节分开。不要既有列表又有字符串——只保留列表,再提供某种“给编译器优化提示“的机制,必要时它可以把字符串布局成连续字节。

由于程序里大多数地方速度并不重要,平时你不必去操这种心。随着电脑越来越快,这个判断只会越来越成立。

少说一点关于实现的事,也会让程序更灵活。规格在写程序的过程中会变——这件事不仅是不可避免的,更是值得期望的。

“essay“这个词来自法语动词 essayer,意为“尝试”。在原本的意义上,essay 是你写来“试着把某件事弄清楚“的东西。在软件里也是一样。我认为有些最好的程序就是“essay“——意思是作者动笔时,并不确切知道自己要写的是什么。

Lisp 黑客早就明白“对数据结构保持灵活“的价值。我们倾向于把程序的第一版写成“什么事都用列表来做“。这种初版可能低效得令人震惊——震惊到你得有意识地不去想它在干什么;就像我至少在吃牛排时,得有意识地不去想它来自哪里。

一百年后的程序员最看重的,会是这样一种语言:你能用尽可能少的力气,拼出一份效率低得离谱的 1.0 版。至少,这是按今天的语言我们会这么描述的。他们自己会说:他们想要一门“写起来容易“的语言。

低效的软件不可耻。可耻的是一门让程序员做无谓工作的语言。浪费程序员的时间才是真正的低效,浪费机器的时间不是。随着电脑变得越来越快,这一点会越来越明显。

我认为“去掉字符串“这件事,今天我们已经可以接受去想了。Arc 里我们就这么做了,看上去是值的——一些用正则表达式描述起来别扭的操作,用递归函数描述起来反倒轻松。

数据结构的这种“扁平化“会走多远?我能想到一些连我自己——已刻意把脑子放宽——都觉得震撼的可能。比如,我们会去掉数组吗?毕竟,数组只是哈希表的一个子集——键是整数向量。我们会用列表去取代哈希表本身吗?

还有比这更震撼的前景。比如 McCarthy(John McCarthy,Lisp 之父)1960 年描述的那个 Lisp 里,没有数字。从逻辑上讲,你不需要把“数字“作为一个单独的概念——因为你可以把整数 n 表示为含 n 个元素的列表。你可以这样做数学。只是低效得令人难以忍受。

实际上没人提议过真的把数字实现为列表。事实上,McCarthy 1960 年那篇论文当时根本没打算被实现——它是一次理论练习,是在尝试给图灵机设计一个更优雅的替代品。后来出乎意料地,有人把这篇论文翻译成了一个能跑的 Lisp 解释器——这时候的数字当然不是用列表表示的;和别的语言一样,是用二进制表示的。

一门编程语言会不会走到那一步——把数字也踢出基本数据类型?我提这个问题不是真的当严肃问题问,更像是在和未来玩一场“懦夫游戏“。这就像那个假想情境:一股无法抵挡之力撞上一件无法移动之物——这里则是一种无法想象地低效的实现,遇上无法想象地丰沛的资源。我看不出为什么不行。未来很长。如果我们能做点什么去减少核心语言里的公理数量,那当 t 趋于无穷大时,这看起来就是该押注的那一边。如果这个想法在一百年后还令人难以接受,也许在一千年后就不会了。

为了说清楚:我并不是说所有数值计算都真的得用列表来做。我说的是,核心语言——在任何关于实现的额外标记进来之前——应当这样定义。实践中,任何想做点数学的程序,多半会用二进制表示数字;但这只是一种优化,不属于核心语言的语义。

另一种烧掉算力的办法,是在应用与硬件之间叠很多层软件。这也是一个我们已经看得到的趋势:很多近来的语言被编译成字节码。Bill Woods(早期 AI/NLP 研究者)曾告诉我一条经验法则——每多一层解释,速度大约掉一个数量级。这一份额外开销,给你买来的是灵活性。

Arc 的最初版本就是这种“多层缓慢“的极端例子,伴随着相应的好处。它是一个写在 Common Lisp 之上的经典“元循环解释器“,与 McCarthy 原始 Lisp 论文里定义的 eval 函数有明显的家族相似性。整件东西只有几百行代码,所以非常容易理解和修改。我们用的那个 Common Lisp——CLisp——本身又跑在一个字节码解释器之上。所以我们这里有两层解释,其中一层(最上层)低效得吓人——而这门语言依然能用。我承认是“勉强能用“,但能用。

把软件写成多层结构,即使在应用程序内部也是一种强大的技术。自底向上编程,意思就是把一段程序写成一系列层,每一层都是为它上面那一层服务的“语言“。这种做法倾向于产出更小、更灵活的程序。它也是通向那个圣杯——可复用性——的最佳路径。语言按定义就是可复用的。你越能把应用的更多部分下推成“用来写这类应用的语言“,你的软件就越能复用。

不知怎么的,“可复用性“这个想法在 1980 年代被绑到了“面向对象编程“上——任凭多少反例都摇不下来。但有些面向对象的软件确实可复用——让它可复用的,是它自底向上的部分,而不是它面向对象的部分。你看程序库:它们之所以可复用,是因为它们就是一种语言,无论是不是用面向对象风格写的。

顺便说一句,我并不预测面向对象编程会消亡。虽然我不觉得它能给好程序员带来很多东西——除了在某些特定专业领域之外——但它对大型组织有不可抗拒的吸引力。面向对象编程提供了一种可持续地写意大利面式代码的办法——它让你把程序当作一系列补丁堆起来。大型组织一向倾向于这样开发软件,我预计这一点在一百年后仍会是真的。

既然在谈未来,那我们最好谈谈并行计算——因为这个想法似乎就住在“未来“里。也就是说,无论你什么时候在谈,“并行计算“似乎总是一件将要在未来发生的事。

未来会赶上它吗?人们至少把“并行计算即将到来“挂在嘴上 20 年了,到目前为止它对编程实践影响并不大。或者说,没有吗?芯片设计者已经必须考虑它了,在多 CPU 电脑上写系统软件的人也必须考虑它了。

真正的问题是:并行性会沿着抽象阶梯往上爬到哪?一百年后,它会影响到应用程序员吗?还是会停留在编译器作者的脑子里、在应用源代码里通常是不可见的?

似乎可以确定的一件事是:并行性的大多数机会会被浪费掉。这是我那个更一般预测——“我们将得到的额外算力大多数会被浪费掉”——的一个特例。我预计:和底层硬件那种惊人的速度一样,并行性会成为“你显式要它,它就给你;但平时不用“的东西。这意味着一百年后我们拥有的并行性,除了在特定应用里之外,不会是大规模并行。我预计对普通程序员而言,它更像是“能 fork 出(派生)一些进程,让它们最终都并行跑起来“。

而这件事,就像“为数据结构指定具体实现“一样,会是你在程序生命中比较晚才做的事——做优化时才考虑。1.0 版本通常会忽略并行计算能带来的任何好处,正如它们也会忽略“为数据指定具体表示“能带来的好处。

除了某些特定应用,并行性不会渗透到一百年后写出来的程序里。如果它真渗透了,那就成了过早优化。

一百年后会有多少种编程语言?最近似乎冒出来一大堆新语言。部分原因是更快的硬件让程序员可以根据应用,在“速度 / 便利“之间做出不同的取舍。如果这是一个真趋势,那我们一百年后的硬件只会进一步推动它。

但一百年后,被广泛使用的语言可能仍只有少数几门。我这么说部分是出于乐观——看上去,如果你做得真的好,是有可能造出这样一门语言的:它既适合写一份慢的 1.0 版,又能在你给编译器以正确优化提示的情况下、在必要时产出非常快的代码。所以,因为我乐观,我打算这样预测:尽管一百年后的程序员会面对一个“可接受效率到极致效率“之间的巨大缺口,他们手上的语言能跨越其中的大部分。

随着这道缺口拉大,profiler(性能分析器)会越来越重要。今天对性能分析的注意力很少。许多人似乎仍相信,得到快应用的办法是写出能生成快代码的编译器。随着“可接受性能“和“极致性能“之间的缺口拉大,会越来越清楚:得到快应用的办法是有一份从前者到后者的好向导。

我说“也许只有少数几门语言“时,没把那种领域专用的“小语言“算进来。我觉得这种嵌入式语言是个很好的主意,预计它们会大量繁衍。但我预计它们会被写成“足够薄的皮“——薄到用户能透过它看到下面的通用语言。

未来的语言由谁设计?过去十年最令人兴奋的趋势之一,是开源语言——Perl、Python、Ruby——的崛起。语言设计正在被黑客接管。到目前为止,结果有点乱,但令人鼓舞。比如 Perl 里有一些震撼性新颖的想法。许多则震撼性糟糕——但这一向是雄心勃勃的尝试的常态。按它当前的突变速度,天知道 Perl 一百年后会进化成什么。

“会做的就去做,不会做的就去教“这条话不对(我认识的最好的几位黑客就是教授);但确实——教别人的人有一大堆事自己做不了。研究施加着拘束性的“种姓“限制:在任何一个学术领域里,都有一些课题“可以做”,另一些“不行“。不幸的是,“可做 vs. 禁做“的分野,通常基于“用论文写出来听起来够不够智识”,而不是“对得到好结果有多重要“。极端例子大概是文学——研究文学的人很少说出对从事文学创作的人有用的话。

科学领域要好一些,但“被允许去做的工作“和“能产出好语言的工作“之间的重叠,小得令人沮丧。(Olin Shivers(Scheme/Lisp 学者)对此曾雄辩地抱怨过。)比如,“类型“看上去是论文取之不尽的源泉——尽管,静态类型似乎排除了真正的宏——而在我看来,没有宏的语言不值得用。

趋势不只是“语言作为开源项目而非’研究’去开发“,而是“语言由那些需要用它的应用程序员来设计,而不是由编译器作者来设计“。我觉得这是个好趋势,预计它会延续。

不像一百年后的物理学——几乎注定无法预测——我认为今天原则上是有可能设计出一门一百年后的用户也会喜欢的语言的。

设计语言的一种办法是:直接写下你希望自己能写的程序——不管有没有编译器能翻译它,也不管有没有硬件能跑它。这样做的时候,你可以假定资源无限。我们今天在脑子里假想“无限资源“的能力,应该和一百年后差不多。

那你想写什么样的程序?最省事的那种。但不完全是——是当你关于编程的想法还没被你目前用的语言污染的时候,最省事的那种。这种污染可能弥漫到这种地步:要克服它需要巨大努力。你会以为,对我们这种懒到家的生物来说,“如何用最少力气表达一段程序“应当是显而易见的。其实,我们对“什么是可能的“的认识,往往被“我们用来思考的语言“限制得太死——以至于“更省事的程序写法“看起来非常令人惊讶。它们是你必须发现的东西,而不是自然而然就掉进去的。

这里有个有用的小窍门:用程序的长度作为“写它要花多少力气“的近似值。当然不是字符长度,而是不同语法元素的长度——大致就是语法树的大小。“最短的程序最省事“未必百分百正确,但近到这种程度——你不如把瞄准目标设为“简短“这个清晰可触的靶子,而不是设为“最省事“那个模糊的、就在它旁边的靶子。于是语言设计的算法就变成了:盯着一段程序问,有没有更短的写法?

实践中,“用一门虚构的百年语言写程序“在不同地方可行性不同——取决于你离内核有多近。排序例程你今天就能写。但今天很难预测一百年后会需要哪些种类的库。想必很多库是为我们今天还不存在的领域准备的。比如,如果 SETI@home(1999 启动的“在家搜寻外星智慧“分布式计算项目)真奏效,我们就需要“和外星人通讯“的库——除非他们足够先进,已经在用 XML 通讯了。

而在另一端,我认为今天就可以设计这门语言的核心。事实上,有人会主张,它在 1958 年就基本被设计好了。

如果百年语言今天就在手上,我们会想用它写程序吗?回答这个问题的一种办法是回头看:如果今天的编程语言在 1960 年就存在,那时会有人想用它们吗?

某种意义上,答案是不会。今天的语言假定的基础设施在 1960 年并不存在。比如一门“缩进有意义“的语言(像 Python),在打印机式的终端上就不太能用。但把这种问题搁一边——假设程序都是写在纸上的——1960 年代的程序员会喜欢用今天的这些语言写程序吗?

我觉得他们会。其中一些想象力较弱的人,把早期语言的“残留物“内嵌进了他们对“程序“的理解,可能会有困难。(不做指针运算怎么操作数据?不用 goto 怎么实现流程图?)但我认为最聪明的程序员要是当时手上有今天的这些语言,是不会有什么困难把它们用得淋漓尽致的。

如果今天我们就有这门百年语言,那它至少能拿来当一份很好的伪代码。用它来写软件呢?既然百年语言对某些应用要能产出快代码,那它大概也能产出在我们今天的硬件上跑得“还不错“的代码。我们也许得给比一百年后的用户更多的优化提示,但仍可能是净赚。

现在我们手上有两个想法,把它们合到一起会冒出有趣的可能性:(1) 百年语言原则上今天就能设计出来;(2) 这样一门语言如果存在了,今天用它写程序也许是好事。把这两条这样并排放着看,你很难不想:为什么不试着今天就把百年语言写出来?

做语言设计时,我觉得心里有这样一个目标、并自觉地把它放在心里,是好事。学开车时教你的一条原则是:对齐车身的方法,不是把车头对准路上画的条纹,而是瞄向远处一个点。哪怕你只关心接下来这十英尺会发生什么,这仍是正确答案。我认为,对编程语言我们也可以、也应该这样做。

注释

我相信 Lisp Machine Lisp 是第一门体现“声明(除了那些关于动态变量的之外)只是优化提示,不会改变正确程序的含义“这条原则的语言。Common Lisp 似乎是第一门把这条明确写出来的语言。

致谢 Trevor Blackwell、Robert Morris、Dan Giffin 通读初稿;感谢 Guido van Rossum(Python 之父)、Jeremy Hylton 和 Python 社群的其他人邀请我在 PyCon 演讲。


本文与另外 14 篇文章一同收录于《黑客与画家》。