四年前,我曾经写过一篇题为“编程中的极简主义”的博客文章,文中我试图提出一个观点,说明为什么在编程项目中应该尽量减少复杂性。现在,我想写一些已经思考了很久的东西,那就是在设计编程语言的时候,我们也应该更有意识地遵循极简主义(minimalistic)哲学。
在我看来,在设计编程语言的时候,有意识地遵循极简主义是一个被严重低估的想法。大多数的现代编程语言都更多地采用了极繁主义(maximalism)的设计方式。快速增加新的特性被视为相对于其他编程语言的竞争优势。一般的想法似乎是,如果你的语言缺少某个特性,那么人们就会选择使用另外一门语言,或者说,增加更多的特性是一种展示进步的简单方式。这种想法是非常肤浅的,而且忽略了许多其他关键因素,它们是一门编程语言成功和繁荣的必要条件,如易学性(learnability)、稳定性、工具支持和性能。
变化与流失
我想说的是,有意将编程语言设计为具有较少的特性,并且随着时间的推移,较少发生变更,这本身就是一个强大的特性。当一门编程语言经常变化时,它必然会导致破坏和流失。工具会过时,代码库需要更新,库会损坏,而且它也会导致人员方面的流失。
我第一次开始使用 C++编程是在 1998 年左右。我已经有几年的时间没有真正接触这种语言了,不得不说,我感到有点迷茫。现在增加了这么多的特性,它已经是一门完全不同的语言了。去年,我想在一个新项目中使用 C++20,但是发现对 G++和 Clang 的支持是如此不完整,以至于模块根本就是一个不可用的特性。我当时的总体印象是,没有足够的人从事 C++编译器的工作,以保持这些编译器处于最新状态。这门语言已经变得如此复杂,而且增加了如此多的新特性,以至于编译器开发人员都已经精疲力尽了。在我看来,C++会慢慢崩溃于自己的重点,我对这一点非常肯定。
许多人忘记了,一门语言要想成功,必须要有良好工具的支持。如果语言和它的特性集不断变化,那么工具就需要不断地更新。C++的众多问题之一就是它的语法难以解析。在 1998 年就已经是这样了。如果在这个问题之上,每隔一两年就加入新的语法变化,使其更加复杂,那么会带来什么样的影响呢?维护 C++工具的人们就会想着去做其他的事情了,使用这些工具的人同样如此。
易学性和人的因素
最近,我和同事们决定将一个C语言代码库移植到Rust。总的来说,我对 Rust 的核心特性集很满意,我觉得在很多方面,它都比 C 和 C++有很大的进步。然而,在我看来,Rust 的主要问题在于它的复杂性。无论是在语法还是语义方面,Rust 都是很复杂的语言。语法可以变得非常冗长,有很多的东西需要了解,有很多规则和不直观的细节,比如在什么地方可以做什么,不可以做什么。它的学习曲线很陡峭,认知负荷很高。
上周,我和同事在进行结对编程的时候,他说“我觉得 Rust 编译器总是在告诉我,我太笨了。”这句话让我很吃惊,因为我也有相同的想法。不知为何,Rust 给人的感觉是不符合人类工程学的,而该语言高度的复杂性肯定会加剧这种感觉,即该语言对用户不太友好。它打破了我们的直觉,而且感觉编译器在不断告诉你,你写的代码是错误的。在我和同事说这番话的两天后,我看到了 Hacker News 上一篇名为“Rust: A Critical Retrospective”的文章,其中对 Rust 的复杂性也有类似的感受。
在很多方面,我觉得如果能够将语言设计成简约的、有较少的概念并且能够选择基元(primitive)进行组合,那么这是让语言更易于学习的好办法。如果编程语言有更少的概念,那么需要学习的东西会更少,你的熟练程度也会快速提升。用更简约语言编写的代码也更易于阅读。如果思考一下 C++语言的代码,我们都会有这样的场景,该语言有许多多余的特性,以至于在典型的工作场所中仅允许使用 C++的子集来编写代码,有些语言特性被明令禁止。这意味着在不同场所编写 C++代码的人将很难读懂对方的代码,因为别人的 C++代码会使用不同的方言来编写。
在某些方面,我觉得有意将复杂性降到最低,并保持较小的特性集是尊重程序员的更好方式。这意味着我们尊重程序员,他们的生活可能会很忙,有很多事情要做,而且他们可能没有足够的时间去阅读数百页的文档来学习我们的语言。编程语言是用户接口,因此,它们应该遵守最小惊喜的原则。尽量减少复杂性也是减少认知负荷和尊重人类局限性的一种方式。人类是能力惊人的生物,但我们基本上也只是会说话的聪明猴子而已。我们只能在工作记忆中保留有限的内容,我们只能考虑到有限的设计限制,我们只能在有限的时间内保持专注。尽管我们有人类的局限性,但一个设计良好的编程语言应该能帮助我们取得成功。
最后,我认为一门语言的复杂性和它给人的直觉会影响其吸引和保留新用户的能力。在我看来,对减少冲突的关注在很大程度上促成了 Python 的最初成功和快速普及。我觉得也可以这样说,当 Python 生态系统的复杂性增加时,许多人会感到很沮丧,例如,在从 Python 2 到 3 的转换过程中,或者当多余的walrus操作符被引入时。
极简主义
到目前为止,我已经多次提到了极简主义,也简单提到了最小惊喜原则。我已经暗示过,极简主义意味着要学习一个较小的特性集和较少的概念。不过,极简主义并不仅仅意味着较小的特性集。它还意味着仔细选择那些能无缝结合在一起的特性。如果我们设计的语言有一个很大的特性集,那么这些不同的特性就会出现组合爆炸,这意味着可能遇到一些语言特性在一起互动不良的情况。
命令式编程语言通常对语句(statement)和表达式(expression)进行语法上的区分。函数式语言则倾向于以一种结构化的方式,将函数体中的所有内容都作为一种表达式。后者更加简约,而且对程序员的约束也更少。有些语言将编译时运行的代码与程序执行时运行的代码区分开来。这种区分往往会增加语言的复杂性,因为往往会出现语言特征的重复,以及对编译器在编译时能够运行哪些代码的随意限制。
在尽量减少惊喜方面,我们要避免引入只在某些情况下出现的奇怪边缘场景。另一个需要避免的重要陷阱是引入程序员可能没有意识到的隐藏行为。这方面的一个例子是 JavaScript 中的相等(==)运算符,它实际上包括对字符串类型的隐性转换,这意味着1=="1"
会被认为是 true。由于这种不理想的隐藏行为,JS 实际上有一个单独的严格相等运算符(===),它不执行隐藏的字符串转换。在我看来,JS 只应该有一个严格的相等运算符,如果你想在执行相等比较之前将要比较的值转换为字符串,那只需要明确地写出来即可。
实现的复杂性
语言设计是很困难的,因为编程语言可能的空间是无限的,所以必须要做出妥协。很难提供硬性的数字来量化如何让某种设计比其他设计更好。在某种程度上可以量化的一些东西是语言的实现复杂性,以及特定语言的实现方式。
我的博士论文中涉及到了 JavaScript ES5 的 JIT 编译器实现。因此,我对该语言的语义以及为使 JavaScript 代码快速运行而必须在幕后进行的所有工作都非常熟悉。在某种程度上来说,这是一个令人沮丧的经历。我确信,JS 和许多其他语言中的许多复杂性以及隐藏行为在本质上对每个人都是不利的。
一门语言中不必要的复杂性对学习这门语言的人是不利的,因为它使这门语言不直观,难以学习。这对每天与语言打交道的程序员来说是不好的,因为这增加了他们的认知负担,使他们更难沟通代码。这对语言实现者和工具维护者来说是不好的,因为这使得他们的工作更加困难,但归根到底,这对最终用户来说也是不好的,因为它导致软件会有更多的缺陷和更差的性能。
举一个不必要的实现复杂性的样例,许多面向对象的语言有这样的想法,这是从 Smalltalk 借鉴来的,“一切都应该是对象”,包括布尔和整数值。同时,这些语言的实现必须在幕后做大量的工作,试图有效地表示整数(作为机器整数),同时向用户展示一个类似于对象的接口。然而,呈现给用户的整数对象的抽象通常与普通的 OOP 对象的抽象并不一样,它是一个漏洞百出的抽象,因为从逻辑上能够重新定义整数值完全没有意义,整数值必须是单例的,能够在整数上存储属性是既愚蠢又损害性能的,所以通常不被允许。
归根结底,整数不是面向对象意义上的对象。它们是一种具有特殊含义的原子值类型。认为“一切都应该是对象”的错误想法实际上并没有在实践中简化什么。我们在欺骗自己,这实际上使语言实现者和程序员的生活都变得更加复杂。
可行的建议
这篇文章更多已经变成了抱怨,这超出了我的预期。批评现状是很容易的,但最终我也会尝试给出一些可操作的建议。我对有志于成为语言设计师的第一个建议是,你应该从小处着手。你的语言是一个用户接口,是一个人们用来与机器交互的 API。API 的面越小,引入意外复杂性和不经意设计错误的风险就越低。
我的第二个建议是,如果可以的话,应该尽量控制语言的规模。将自己限制在一个较小的特性集上,这可能意味着你要选择那些没有重叠的特性,它们能够给程序员带来最大的表现力和最大的价值。如果确实想发展你的语言,那就慢慢来吧。花一些时间在你的语言中编写代码,并研究你所做的设计改变会带来的潜在影响。
增加新的特性是很容易的,但是如果你增加了新的特性,人们开始使用它们,就很难甚至不可能收回这些特性了,所以要明智地选择。记住,你不需要取悦所有人,也不需要对每个特性请求都说“好”。没有任何一种语言或工具可以满足所有的使用场景,而且在我看来,试图这样做就是一个错误。
最后,请记住,语言设计是一门艺术。它是众多不同约束条件的微妙平衡,就像用户界面设计一样。Brainfuck 是一种非常小的语言,只有很少的概念,但没有人会说它具有表现力或优雅。Lisp 被许多人认为是现存的最漂亮、最优雅的语言之一,但我的博士生导师是一位 Scheme(Lisp 编程语言的方言之一——译者注)的狂热爱好者,他有写代码时使用单字母变量名和很少添加注释的习惯。优雅的语言不会自动产生优雅的代码,但如果你以身作则,可以鼓励实现良好的编码实践。
相关阅读:
和Rust一样好,编程更安全?三年实践、员工态度反转,英伟达用 SPARK 换掉 C
评论 1 条评论