小语言才是编程的未来

本文最初发布于 chreke 的个人博客。

我坚信,“小语言”——设计用来解决非常具体的问题——是编程的未来,特别是在阅读了 Gabriella Gonzalez 的著作《编程历史的终结》并观看了 Alan Kay 的演讲Programming and Scaling之后。你也应该看看,因为它们都很棒。下面,我会解释下我所说的“小语言”是什么,以及为什么它们很重要。

什么是“小语言”

我认为,“小语言”这个词是 Jon Bentley 在其同名文章“小语言”中创造的,他给出了以下定义:小语言是专门针对特定问题领域的语言,常规语言提供的许多特性它都没有。

例如,SQL 是一种描述数据库操作的小语言,正则表达式是一种用于文本匹配的小语言,Dhall是一种用于配置管理的小语言等。这类语言还有其他一些名称,如领域特定语言(DSL)、面向问题语言等等。

然而,我喜欢“小语言”这个词,部分原因是“DSL”这个词已经承载了太多的东西:从提供流体接口(Fluent Interface)的库到像 SQL 这样的成熟查询语言,还有就是“小语言”突出了它们“小”的特点。

为什么我们需要小语言?

如今的大多数软件很像埃及金字塔,数百万块砖堆叠在一起,缺少结构完整性,只是借着蛮力和成千上万的奴隶完成。

—— 摘自与Alan Kay的对话

在软件工程社区,我们真正遇到的一个问题是:随着应用程序复杂性的增加,其源代码的大小也会增加。然而在很大程度上,我们理解大型代码库的能力仍然没变。根据 Sourcegraph 公司 2020 年的一项调查(大代码的出现),大多数受访者表示,他们代码库的规模导致了以下一个或多个问题:

  • 新员工很难适应

  • 因为缺乏对依赖性的理解而造成代码损坏

  • 代码变更难以管理

更糟糕的是,应用程序似乎在以惊人的速度增长:在 Sourcegraph 的调查中,大多数受访者估计,他们的代码库在过去十年中增长了 100-500 倍。举个例子,Linux 内核在 1992 年的时候是从大约 1 万行代码开始的,20 年后,它的体量约为 3000 万行。

这些代码是从哪里来的?“更多的功能”不足以解释这种代码量的增长。相反,我认为这与我们开发软件的方式有关。向程序中添加新功能的一般方法是将它们堆叠在已有的功能上,这与构建金字塔的方式没有什么不同。问题是,就像金字塔一样,后面每一层所需要的砖都比上一层多。

逆势而动

你真的需要数百万行代码才能创建一个现代操作系统吗?2006 年,Alan Kay 和他在STEPS项目中的合作者开始向这一假设发起挑战:

科学的进步是在实证调查和理论模型的交织中完成的,所以作为科学家,我们的第一个问题是:如果我们为个人计算现象建立一个工作模型,那么它是会简化为适用于所有电磁频谱的麦克斯韦方程组(Maxwell’s Equations)或者可以放在衬衫口袋里的美国宪法(US Constitution)这样简单的东西,还是会非常的杂乱无章(或实际上非常复杂),以至于需要像美国法律体系(或当前的软件实践)那样需要“3 立方英里的判例法”?答案几乎可以肯定,是介于两者之间,在这种情况下,如果能够证明它更接近简单的一端,而不是那巨大而混沌的另一个极端,那将非常有趣。

所以我们要问:本质上,个人计算体验(包括操作系统、应用程序和其他支持软件在内)是 20 亿行代码,还是 2 亿、2000 万、200 万、20 万、2 万、2000 行代码?

STEPS 2007进展报告,第 4-5 页

Dr. Kay 提到的麦克斯韦方程组是一组描述电磁学、光学和电路学的方程组。很酷的一点是,尽管涉及的范围很大,但它们却非常简洁:


它们如此简洁的一个原因是使用了Del符号(如∇)描述向量计算操作。需要注意的是,Del 并不是一个真正的运算符——它更像是一种简写,为的是使向量计算中的一些方程更容易处理。

是否有可能为编程创造出同等的 Del 符号呢?就像 Del 可以帮助我们使向量运算更易于管理一样,是否有符号可以以同样的方式帮助我们推断程序?这个问题是推动 STEPS 项目的“强大想法”之一:

我们还认为,针对要解决的问题创建合适的语言可以使问题更容易解决,使解决方案更容易理解而且也相对较小,这恰好符合我们“active-math”方法的精神。这些“面向问题语言”将被创建出来,用于大大小小的问题以及不同的抽象和细节层次。

STEPS 2007进展报告,第 6 页

其思想是:当你开始在应用程序中寻找模式时,就可以用一种小语言对它们进行编码——这种语言让你可以用一种比其他抽象方法更紧凑的方式来表示这些模式。这不仅可以防止应用程序不断变大的趋势,而且在实际的开发过程中还会使用代码库缩小

我觉得,STEPS 项目一个特别令人印象深刻的成果是Nile,这是一种描述图形渲染与合成的小语言。其目标是使用 Nile 实现与Cairo(一种用于各种自由软件项目的开源渲染器,有大约 44000 行代码)同等的功能,但 Nile 代码大约只有 300 行。

为什么不用高级语言?

然而事实证明,Ada 不会成为杀死软件生产率怪物的银弹。毕竟,它只是另一种高级语言,这类语言的最大回报来自于最初的过渡阶段,从机器附带的复杂性上升为更抽象的分步解决方案。一旦这些意外事件得到解决,剩下的意外事件就比较小了,解决它们的回报肯定会变少。

—— Frederick P. Brooks,《没有银弹

可能有人会问,为什么不能发明一种更高级的通用语言呢?就我个人而言,我相,通用语言的表达能力已经到了回报递减的阶段。如果有更高级的语言,那它会是什么样子呢?以 Python 为例,它非常高级,看起来已经很像伪代码了。

通用语言的问题是,你仍然需要将问题转换为算法,然后用目标语言表达这个算法。高级语言非常擅长描述算法,但除非目标是实现算法,否则这只是附带的复杂性。

写这篇文章时,我想起了一个关于 Donald Knuth 的故事:Jon Bentley 邀请 Knuth 在其专栏《编程珠玑》中展示他的编程风格;他还邀请 Doug McIlroy 对 Knuth 的项目进行评论。其任务是计算给定文本中的词频。

Knuth 的解决方案是用 WEB 精心编写的,这是他自己的 Pascal 编程变体。他甚至还加入了一个专门用于记录单词数量的数据结构,所有这一切用了不到 10 页代码。虽然 McIlroy 毫不犹豫地称赞了 Knuth 解决方案的精巧,但他对程序本身并不是很满意。作为评论的一部分,他用 Shell 脚本、Unix 命令和小语言编写了自己的解决方案:

tr -cs A-Za-z '\n' |
tr A-Z a-z |
sort |
uniq -c |
sort -rn |
sed ${1}q

虽然对于非 Unix 骇客来说,这段代码可能读起来有点困难(McIlroy 可能也会承认这一点,因为他认为提供一个带注释的版本比较合适),但可以说,与 10 页的程序相比,这个总结性的回复无疑更容易理解。

Unix 命令是为操作文本而设计的,这就是为什么用它可以编写出如此紧凑的单词计数程序——是不是可以将 Shell 脚本看成是文本操作的“Del 表示法”呢?

少即是多

上面的 Unix 命令示例说明了小语言的另一个特征:语言功能较弱、运行时功能较强。Gonzalez 在《编程历史的终结》一书中指出了如下趋势:

当我们研究上述趋势时,会发现一个共同的模式:

– 把用户关注点变成运行时关注点,它

– ……使程序更接近纯粹的数学表达式,并且

– ……极大地增加了运行时的复杂性。

正则表达式和 SQL 只能分别用于表达文本搜索和数据库操作。这与 C 语言形成了鲜明的对比,C 语言没有运行时,你可以用它表达任何在冯·诺依曼体系结构上可能的东西。像 Python 和 Haskell 这样的高级语言介于两者之间:它们帮你完成内存管理,但你仍然可以使用图灵完备语言的全部功能,也就是说,你可以表达任何可能的计算。

与 C 语言相比,小语言处于能谱的另一端:不仅计算机的体系结构被抽象了,其中一些语言还限制了你可以表达的程序的种类——它们在设计上就是图灵不完备的。这听起来可能非常有局限性,但实际上,它为优化和静态分析打开了一个全新的可能性维度。而且,就像抽象掉内存管理可以消除一整类 Bug 一样,抽象掉尽可能多的算法工作,也有可能消除更多的 Bug。

静态分析

功能不那么强大的语言更容易推理,并且可以提供比通用语言更强的保证。例如,Dhall是一种用于生成配置文件的全函数式编程语言。因为你不想冒部署脚本崩溃的风险或者把它们放入无限循环,Dhall 程序可以保证:

  1. 不崩溃,并且

  2. 在有限时间内终止。

第一点是通过不抛出异常来实现的;任何可能失败的操作(例如获取一个可能为空的列表的第一个元素)都返回一个 Optional 结果,该结果可能包含也可能不包含值。第二个点——保证终止——是通过不允许递归定义实现的。在其他函数式编程语言中,递归是表达循环的主要方式,但在 Dhall 中,你必须依赖于内置的fold函数。缺少一般的循环结构也意味着 Dhall 不是图灵完备的;但因为它不是一种通用编程语言,所以它不需要是完备的(不像 CSS)。

如果语言很小,就更容易推理了。例如,对于任意一个 Python 程序,都很难确定它有没有副作用,但在 SQL 中就很简单——只需检查查询是否以 SELECT[5]开始。

对于 Nile,STEPS 团队认为需要一个图形化调试器[9]。Bret Victor(是的,就是那个做过“原则性发明”演讲的 Bret Victor)发明了一个工具,它可以告诉你在屏幕上绘制特定像素所需的确切代码行。你可以在 YouTube 上观看Alan Kay的演示,也可以自己尝试一下。因为 Nile 是一种很容易推理的小语言,所以才可能有像这样的工具——想象一下,尝试用 C++编写的图形代码做同样的事情!

速度追求

功能更强大的编程语言不仅会增加 Bug 的可能性,还会对性能造成不利影响。例如,如果一个程序不是用算法表达的,那么运行时就可以自由选择自己的算法;如果我们能证明它们生成的结果相同,就可以用速度较慢的表达式代替速度较快的。

例如,SQL 查询并不规定一个查询应该如何执行——数据库引擎可以自由使用它认为最合适的任何查询计划,比如,它是应该使用索引,索引组合,还是直接扫描整个数据库表。现代数据库引擎还收集列的值分布信息,因此,它们可以动态选择统计学上最优的查询计划。如果查询是用算法的方式描述的,就不可能这样了。

使Nile语言如此紧凑的“秘密武器”之一是Jitblt,这是一种用于图形渲染的即时编译器。从STEPS团队和Cairo团队的讨论中可以清楚地看到,Cairo 的很多代码都是专门用于像素合成操作的手工优化;理论上,这些工作可以交给编译器。Cairo 团队的 Dan Amelang 自愿实现了这样一个编译器,也就是Jitblt。这意味着图形管道中的优化工作可以从渲染内容的纯数学描述中分离出来,使得 Nile 的运行速度可以像最初手工优化的 Cairo 代码一样快。

小语言,大潜力

那么,STEPS 项目发生了什么呢?他们最终得到的是相当于“3 立方英里判例法”的代码,还是设法创建了一个小到可以印在 T 恤上的操作系统?STEPS 的最终结果是 KSWorld,这是一个完备的操作系统,包括文档编辑器和电子表格编辑器,代码最终有大约 17000 行[10]。虽然要印下所有这些代码需要一件非常大的 T 恤,但我仍然认为这很成功。

KSWorld 的创建似乎可以证明小语言的巨大潜力。然而,仍有许多未解之谜,例如:这些小语言相互之间应该如何交互?是否应该将它们编译成通用的中间表示形式?或者同时存在多种不同的运行时并通过通用协议(例如 UNIX 管道或 TCP/IP)相互通信?或者每种语言都足够小,可以用各种不同的宿主语言重新实现(比如正则表达式)?又也许,未来的道路会结合所有这些方法?无论如何,我都相信,我们需要想出一种不同的软件构建方法。也许小语言会成为这个故事的一部分,也许它们不会——重要的是,我们要停止互扔砖头的做法,想出更好的方法。

延伸阅读

  • Connexion是 Zazzle 开发的一个开源 API 框架;它的突出之处在于它可以根据OpenAPI规范自动生成端点;通常,你会使用OpenAPI来描述现有 HTTP 服务的端点,但Connexion却采用了相反的方式:给定一个 OpenAPI 模式,它将配置一个带有端点、验证逻辑和在线文档的 API 服务器。

  • Catala是一种声明式语言,用于将法律文本翻译成可执行的规范。因为它支持非单调推理(即后面的语句可以抵偿或进一步限定前面的语句),它允许用与编写法律文本大致相同的方式来表达程序,例如,作为一组语句,可以通过添加新语句来修改或扩展。

  • Racket是一种 Lisp 方言,专门为创建新语言而设计(这种技术有时被称为面向语言编程)。我自己并没有太多的时间摆弄 Racket,但它看起来是一个非常适合创建“小语言”的工具。如果你感兴趣,可以阅读“使用Racket创建语言”的教程。

  • 虽然 STEPS 项目已于 2018 年结束,但所有的结果都可以从VPRI Writings页面上获取。

参考资料:

https://www.phoronix.com/news/Linux-Git-Stats-EOY2019

https://www.vpri.org/pdf/tr2009016_steps09.pdf

https://news.ycombinator.com/item?id=1803815

https://homepages.cwi.nl/~storm/teaching/reader/BentleyEtAl86.pdf

https://news.ycombinator.com/item?id=15187150

https://accodeing.com/blog/2015/css3-proven-to-be-turing-complete

https://www.vpri.org/pdf/tr2007008_steps.pdf

https://www.vpri.org/pdf/tr2012001_steps.pdf

原文链接:https://chreke.com/little-languages.html

本文文字及图片出自 InfoQ

你也许感兴趣的:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注