Water - A Programming Language
- Date
- Tags
- notes
- programming language
- compiler
§Abstract
编译器类型推断已经成为现代编程语言的共有特性,它显著提升了开发效率以及程序的可读性和可维护性。
本文将深入探讨 Structural Typing 类型系统的优势,以及类型推断技术与开发工具的结合能为编程语言设计带来了哪些新的可能性,最后试图探索编程语言在未来的发展方向,希望能为读者带来有价值的思考。
在此基础上,最后一章节还将介绍拙作 Water。Water 是一个静态的、基于类型推断的、拥有垃圾回收功能且以字节码为基础的解释型编程语言。它支持 Static Typing 与 Structural Typing 两种类型系统,它的灵感源自于一些现代编程语言,如 Go、Rust 和 Java,以及,以考虑如何与现代开发工具充分结合为基础,设计的一门编程语言。
§绪论
§研究背景
随着第一个高级编程语言 Fortran 的面世,编程语言的数量呈爆发式增长,至今依旧百花齐放。然而,由于近年来软件行业的飞速发展,编程语言设计也将遇到前所未有的挑战。
因此,在设计一门新的现代编程语言前,应先对现有的现代编程语言进行充分研究。唯有先了解现代编程语言的进化过程以及它们所遇到的挑战,才能窥探未来编程语言的发展方向,从而做出更好的设计。
§主流编程语言及开发工具的发展现状
§类型系统的现状
在编程语言的类型系统方面,动态类型语言在过去凭借其易用性和灵活性而备受欢迎,但随着软件项目规模的扩大,类型安全成为了一个挑战。因此,越来越多的动态类型语言开始向静态类型语言转变,以提高代码的可维护性和可读性。与此同时,静态类型语言通过引入类型推断技术来简化代码语法,以便于编写和维护。
而在最近几年,动态类型语言的 Duck Typing 和静态类型语言的类型检查相结合,衍生出了一种新的类型系统 Structural Typing。由于这种类型系统因其兼顾灵活性与安全性受到了许多开发者的亲睐,也因此许多现代编程语言也开始在不同程度上支持 Structural Typing 的类型系统。
§开发工具的现状
而在开发工具的发展方面,LSP(Language Server Protocol) [*1] 和 Inlay Hints [*2] 的诞生对编程语言设计产生了许多革命性的影响,促使开发者们也开始重新审视对编程语言的设计。
其中最明显的例子是,起初静态类型语言的类型推断关键字并不太流行,究其原因是许多人认为使用类型推断关键字会影响代码的可读性。但是,通过类型推断技术和 Inlay Hints 技术的配合,从此将类型推断语法推向了主流。
§文献综述
§背景
在 2014 年,加州大学戴维斯分校曾通过分析 GitHub 代码库来研究编程语言与代码质量之间的关系 [*3]。该研究发现,编程语言的设计确实对软件质量有一定的影响,尤其是强类型语言相比于弱类型语言更能提高代码的质量,同时静态类型语言相比于动态类型语言也有着更好的表现。虽然使用大数据对代码质量进行统计可能存在一些不确定性,但这一结论仍然得到了大多数人的认同。
因此,许多编程语言一直在探索语言设计层面的最佳实践。在过去,那些强调易用性的编程语言在项目规模扩大后往往变得难以维护,而那些注重易读性的编程语言则需要用户编写更多额外的标注,如静态类型声明、变量的生命周期、类型和接口定义等,以使得语法更容易进行静态分析,从而提高项目的可维护性。但值得注意的是,可维护性并不等同于易于维护,编写过多的额外标注还会降低开发效率。也因此,在考虑易用性与易读性之间如何折衷,成为大多数主流编程语言的一个重要考量。
§Structural Typing 类型系统
§前言
编程语言的发展一直是一个不断进化的过程。只有了解它们所遇到的挑战,才能窥探未来编程语言的设计方向。
在本章节中,将会探究动态类型和静态类型两种传统的类型系统的优势,以及分析它们所遇到的挑战,由此推理出诞生 Structural Typing 类型系统的原因。
在最后,也将分析 Structural Typing 与传统类型系统相比又具备怎样的优势与劣势。
§动态类型语言的发展
许多主流动态类型语言出现向静态类型语言转变的倾向,其目的是提高代码的可维护性和可读性。
TypeScript[*4] 是一种基于 JavaScript 的增强编程语言,除了提供更丰富的语法特性,还具备静态类型检查功能。TypeScript 的出现引领了一股静态类型语言的潮流,越来越多的团队开始使用它来开发大型应用程序。
类似地,Mypy [*5] 是一种为 Python 开发的静态类型检查器,允许用户编写带有静态类型标注的 Python 代码,并对代码进行静态类型分析和检查。这种思想得到了 Python 官方的认可,并在 Python 3.5 中正式发布了 PEP 484 - Type Hints [*6]。此后,Python 的 Typing 方面也得到了大力推广和发展。
使用 JavaScript 和 Python 等动态类型语言编写规模较大的项目,它们经常需要面临着类型安全方面的挑战,而静态类型检查器则恰好能弥补这个缺陷。
通过研究发现,动态类型语言的发展趋势是朝着静态类型检查的方向发展,以提高代码质量和可维护性。
// JavaScript
let i = 42
let s = '42'
let date = new Date()
// TypeScript
let i: number = 42
let s: string = '42'
let date: Date = new Date()
对比 JavaScript 代码与 TypeScript 代码
# Python
i: int = 42
s: str = '42'
ex: Exception = Exception()
带有静态类型标注的 Python 代码
§静态类型语言的发展
C++ 和 Java 等当下比较流行的静态类型语言也存在一个共同的发展方向,即通过精简代码的语法使代码更易于编写和维护。
例如,在 C++ 11 中正式发布的 auto 关键字,允许用户在声明变量时使用 auto 关键字替代原先需要显式指定的变量类型(见 图 1),改由编译器自己完成变量类型的推断。实际上 C++ 的作者 Bjarne Stroustrup 在 1984 年已经在他的 CFront 编译器实现了 auto 关键字,但由于与 C 语言兼容性问题被迫将其删除。而在 2003 年再次出现提及 auto 关键字的提案 “Decltype and auto” [*7] 中间仍发生了许多争论,最终 auto 关键字在 2011 年的 C++ 11 中才得以正式发布。但事实上,从 auto 关键字的提出开始至今,反对的声音就没有停止过。
关键字 auto 对代码的简化效果非常显著,但反对的论据种类繁多,在大多数人看来,最有力的反对理由是 "滥用 auto 可能会影响代码的可读性"。
如 图2 所示,仅从该代码片段,开发者难以判断出变量 x 的实际类型,虽然可以借助编辑器跳转到 getX() 的函数声明中查看其返回值类型,但随着变量数量的增加,可能需要在多个函数之间来回跳转以查看变量的实际类型。因此更多人提倡仅在右边表达式的类型显而易见的时候才使用 auto 关键字 [*8]。
在实际项目中,许多变量都由其他函数返回,这种时候的类型并不显而易见,这也是 auto 关键字一直不特别流行的原因。直到几年前,出现了一项称为 “Inlay Hints” 的技术,随着它的出现也逐渐使得类型推断语法成为主流,至于这项技术,将会在后续章节进行详细介绍。
但从整体而言,精简代码语法是主流静态类型语言的发展趋势,目的是为使得代码更易于编写和维护。
// c++ 03
int a = 42;
std::string s = "42";
std::vector<std::string> v = std::vector<std::string>();
typeof(alpha*(u-v)*transpose(w)) x = alpha*(u-v)*transpose(w);
// c++ 11
auto a = 42;
auto s = "42";
auto v = std::vector<std::string>();
auto x = alpha*(u-v)*transpose(w);
图 1: C++ 11 的 auto 关键字的使用
auto x = getX();
图 2: 关键字 auto 对代码可读性的影响
§Structural Typing 与传统类型系统的差异
通过前两章节的分析可以发现,动态类型语言和静态类型语言正在相互汲取对方的优点,同时也朝着相似的方向发展。
动态类型语言正在增加类型标注的语法,以便更多漏洞能够在编译阶段被发现。静态类型语言则在这方面更具优势,因此它们开始为语言增加自动类型推断的语法,以使得使用者编写的代码具有更低的耦合度、更通用、更灵活和更具可重用性。不同的是,动态类型语言拥有更灵活的类型推断方式,例如天然支持的 Duck Typing [*9](“如果它走路像鸭子,游泳像鸭子,叫声像鸭子,那么它可能就是一只鸭子”),可能比静态类型语言更容易支持 Structural Typing [*10]。
本小结将通过一个演进的例子,介绍 Structural Typing 的由来,并分别阐述 Duck Typing、Static Typing 以及 Structural Typing 的用法与差异。
-
如
图 1所示,在doFly()函数中,调用参数flyable的fly()方法,并没有要求传入的实参类型。根据 Duck Typing 的规则,只要实参对象中包含fly()方法,doFly()就能正常运行。但如果传入的实参没有实现fly()方法,则可能会在运行时抛出flyable.fly is not a function的异常。为了将运行时错误提前到编译时,静态检查应运而生。 -
如
图 2所示,可以通过使用 TypeScript 的 Static Typing 限制函数参数flyable的类型,因此Bird类需要显式实现Flyable接口,因为doFly()函数的参数必须是实现Flyable接口的类的对象,这也是静态类型语言区别于动态类型语言的主要区别之一。 -
另一种 TypeScript 中的类型检查方式是 Structural Typing,这与 Static Typing 有些类似。如
图 3所示,doFly函数明确要求参数是实现Flyable接口的类的对象。但是,我们发现类Bird并没有显式实现Flyable接口,仅仅是在Bird中拥有了与Flyable相同的成员fly(),并且还允许Bird和Flyable是来自相互间没有依赖的两个文件。这与 Static Typing 不同的是,如果两个类型具有相同的成员结构,那么它们就被 Structural Typing 认为是相同的类型。这种方式可以使得不同的类型实现相同的结构和成员,从而具有相同的功能,这样就可以在不同的类型之间共享代码,避免了代码的重复编写。在大型项目中,这种方式可以有效地提高代码的复用性和可维护性。Structural Typing 非常像 Static Typing 和 Duck Typing 的结合体,因此也会被称为 Static Duck Typing。

- JavaScript Duck Typing

2. TypeScript Static Typing

3. TypeScript Structural Typing
§支持 Structural Typing 的现代编程语言
Go 语言是 Google 在 2009 年推出的一种语言,它的设计思想遵循 “如果有东西能做到这一点,那么它就可以在这里使用” [*11] 的原则,因此也支持 Structural Typing。Andrew Gerrand 曾指出,“这种思想的好处在于,它能够解耦代码,从而使代码更灵活。这意味着可以事后声明接口,以实现代码复用,而不是像静态类型语言一样在一个地方声明接口,然后再在实现它的地方声明它。我们不是通过建立类型层次结构来构建系统,而是通过建立满足小接口的小部件,然后我们将它们组合在一起" [*12]。
近年来,一些新兴语言如 Scala、Kotlin 和 Rust 也在不同程度上支持 Structural Typing。此外,Python 3.8 也开始支持 Structural Typing [*13] 图 4。

4. Python 类型系统的发展过程
对于熟悉 C++ 的朋友来说,他们可能知道在 C++ 中可以使用 template 实现类似于 Duck Typing 的语法 图 5。虽然 C++ 在编译时对类型进行检查,但是否支持 Structural Typing 还存在争议。然而,在 2020 年发布的 C++20 的 concept [*14] 特性对 template 的参数有类似接口的限制,使其与 Structural Typing 的语言更加相似 图 6。

5. C++ Duck Typing

6. C++ Like Structural Typing
§本章小结
本章节的分析表明,动态类型语言和静态类型语言正在相互汲取对方的优点,实际上还可以发现许多动态类型语言有转化为静态类型的倾向。另一边,静态类型语言通过类型推断技术也能在一定程度上缓解标注过多的问题。
在过去,动态类型语言凭借其易用性和灵活性而备受欢迎,但随着软件项目规模的扩大,类型安全也变成了一个挑战。因此,越来越多的动态类型语言开始向静态类型语言转变,以提高代码的可维护性和可读性。与此同时,静态类型语言通过引入类型推断技术来简化代码语法,以便于编写和维护。
最近几年,结合动态类型语言的 Duck Typing 和静态类型语言的类型检查,衍生出了一种新的类型系统 Structural Typing。许多现代编程语言也在不同程度上支持这种更灵活的类型系统。
§LSP 和 Inlay Hints
§前言
计算机编程领域的发展离不开工具的不断创新和发展。编译器和解释器只是编程语言发展的开始,而后续的辅助工具的出现不仅提高了代码的可读性和可维护性,也在一定程度上影响了编程语言的设计。本章节将讨论开发工具对编程语言设计所带来的影响,并且重点介绍了 "Inlay Hints" 工具的出现所带来的重大影响。此外,我们还将探讨前面章节提到的 “滥用类型推断语法可能会影响代码的可读性” 这一问题的解决方法。最后,我们将探讨开发工具和编程语言结合所带来的可能性,以期展望未来编程语言设计的发展方向。
§LSP (Language Server Protocol)
当下的代码编辑器相较于以前,不再仅仅依赖于 ctags 实现符号检索、代码跳转和代码自动补全等基础功能。LSP(Language Server Protocol 的出现,让代码编辑器可以提供更加精准的符号表、代码重构、代码结构优化以及自动填充语句等高级功能。其中,一些编程语言的官方团队甚至开始维护自己的语言服务器,例如 Go 的 gopls 以及 Rust 的 rust-analyzer,这使得很多支持 LSP 的编辑器能够与成熟的集成开发环境(IDE)不相上下。毕竟,由最熟悉该编程语言的人编写语言服务器,其质量和代码可重用性都将更胜一筹。实际上,rust-analyzer 的开发正是基于 Rust 编译器前端。
与传统的代码检索工具相比,使用语言服务器最大的好处是使编辑器能够深度理解编程语言的语法和语义。LSP 可以通过语言服务器自动推断出代码中的变量名和其他标识符,大大提高了编码效率和代码质量。如 图 1,在 Go 语言中,LSP 可以自动推断变量名 bar,从而让程序员在编写代码时省去了一些琐碎的工作。此外,LSP 还可以提供诸如自动重命名变量或类的功能,修改类及其父类的方法符号表等高级功能。这种智能化的代码生成功能不仅可以提高开发效率,还可以减少因手动编写代码而引起的错误。

- Go Code Generate
不仅如此,使用 LSP 还能提供更为精确的签名信息,如 图 1 所示。左侧展示了在编辑器中编写 Java 代码,并使用编辑器浏览 ServerSocketChannel.open() 方法签名的功能,右侧则是 ServerSocketChannel.open() 的源代码及其代码注释。可以看到,编辑器生成的签名还附带非常清晰的注释格式,这得益于注释中使用特定的语法结构。

Javadoc
由于 LSP 充分发挥了语言服务器的推断功能,这意味着如今编程语言的设计,可以考虑哪部分的代码需要手动编写,而哪部分的代码可以自动生成。以及使用特定的语法结构如 javadoc,可以以非常清晰的方式呈现。
§Inlay Hints
伴随着代码编辑器的飞速发展,近年来出现了一种有趣的功能,通常被称为 “Inlay Hints” 或 “Virtual Text” [*15]。Inlay Hints 指的是在代码中显示附加信息,通常通过只读的虚拟文本片段穿插在代码中的方式呈现内容。图1 展示了一个在编辑器中使用 Inlay Hints 功能的案例,其中 C++ 语言使用类型推断语法 auto 定义变量,编辑器在 LSP 的帮助下为变量显示类型推断后的实际类型。

- C++ Inlay Hints in Editor
在前面的章节中提到,在右边表达式的类型不明显的情况下,使用 auto 可能会影响代码的可读性。但是,结合代码编辑器的 Inlay Hints 功能似乎可以弥补这个缺陷(图2)。现在,基本上所有主流的 C++ 编辑器都支持 Inlay Hints,因此许多 C++ 项目也开始在更多的地方使用 auto 关键字。此外,从 C++ 17 中的新特性 CTAD(Class Template Argument Deduction)也可以看出 C++ 委员会对类型推断的认可(图 3 显示了在编辑器中显示的 pair 变量推断后的类型,笔者手头没有支持 CTAD 的 Inlay Hints 编辑器)。

2. C++ Inlay Hints with Function in Editor

3. C++ Class Template Argument Deduction
因此,随着 Inlay Hints 技术在近年来被大量推广,许多编程语言也陆续支持类型推断语法。
§本章小节
在当今的软件开发领域,编程语言仅仅是软件工程中的一部分,而编程语言的开发者们必须考虑如何使自己的语言与各种工具更好地配合使用。这样一来,开发者们不仅仅需要设计一门语言,而是需要构建一个生态系统,以方便使用者们使用和开发出高质量的软件。
例如,从前的编程语言可能只需要提供一个编译器或解释器来将代码转化为机器语言或字节码,以便于计算机可以执行它。而现代编程语言为了提高代码的可读性和可维护性,开发者们还需要开发各种小工具,如编辑器插件,语言服务器,调试器等。这些工具可以帮助使用者在编写代码的同时, 提供代码语法高亮、自动补全、自动生成、代码重构、Inlay Hints 以及断点调试等各种功能。
开发者们还需要使用各种开发工具,如IDE、代码编辑器、调试器等等。这些工具可以帮助使用者在编写代码的同时,提供代码自动完成、语法高亮、断点调试等各种有用的功能。另外,为了保证代码的质量,还需要开发静态分析工具(如Lint)来检查代码是否符合最佳实践和规范。
除了上述工具之外,还有很多其他的工具可以帮助使用者提高效率和代码质量,例如测试框架、包管理工具和构建工具等等。这些工具的出现使得使用者们可以更加专注于实现业务逻辑和创新,而不必浪费太多时间在重复性的工作上。
因此,现代编程语言的设计者们必须考虑如何使自己的语言与各种工具更好地配合使用,以便于使用者们可以更加高效地开发出高质量的软件。
§探索未来编程语言的设计
§前言
如果说,过去设计编程语言时需要在易用性和易读性这个光谱之间寻求平衡,那么 LSP 和 Inlay Hints 的出现,无疑为我们缩小了这个光谱的两端,使得编程语言设计更能够兼顾二者的优势。虽然易用性和易读性之间的光谱仍然存在,而这也正是编程语言能够不断发展的重要因素之一。
影响编程语言设计的差异往往并不仅仅在一个维度上,另一个带来差异的因素则是对性能的要求。例如,在 IO 密集型场景下,语言通常可以隐藏底层细节,并提供更高级别的抽象,使得代码更加智能化。这种智能化的语法可以帮助开发人员更快地构建程序,也更易于维护。但在计算密集型的场景下,进行底层细微操作是常见的需求,过于高级别的抽象容易增加用户的心智负担,从而更愿意选择功能较为简单的语言。
另一方面,编程语言的多样性和灵活性也在不断增强,例如近年来流行的 Rust、Go 和 Kotlin。同时,编程语言之间的相互借鉴和交流,以及开源社区的发展,也将促进编程语言的发展。这些趋势都将为未来的编程语言和技术发展带来更多的机会和挑战。因此,编程语言通常是在多维度的考量下进行设计的,我们难以对其未来的发展趋势作出全面的一般性陈述。
因此,在本章节中,将基于 LSP 和 Inlay Hints 的功能,提出一个新的设计维度 “代码生成” 与 “Inlay Hints”。并且,基于类型推断技术,推理出基于纯 ”Inlay Hints“ 的 Structural Typing 语言的模样。
§探索未来的编程语言
对于未来的编程语言,笔者认为其设计将会更加注重与工具之间的协作配合,以提供更好的开发体验。例如,类型推断和 Inlay Hints 的搭配在近年来得到了广泛的使用,已经有许多编程语言已经支持类型推断的语法。
而随着 LSP 使编辑器与编译器深度结合,更多高级的代码重构和代码生成都将得以实现。由于代码生成和 "Inlay Hints" 都基于 LSP 的类型推断功能,且二者也都能使得编程语言变得易于使用,因此笔者认为未来编程语言的设计可能会需要考虑一个新的维度 —— ”代码生成“ 和 ”Inlay Hints“ 之间的平衡。也就是说,哪些部分需要留在代码层面提供约束,哪些部分可以由编译器自动推断,仅使用 Inlay Hints 在编辑器进行提示。
下面我们将使用一些简单的例子说明一下关于 ”代码生成“ 和 ”Inlay Hints“ 的差异(由于后续章节会介绍拙作 Water,因此示例的代码将会开始使用类似 Water 和 Rust 的语法)。
如 图 1 所示,我们完全可以基于类型推断设计出一门无需标注类型的静态类型语言,这在 Inlay Hints 的配合下,看上去非常接近传统的静态类型语言。这段简单的程序所描述的是,使用 BigDecimal 和 I32 两种类型,分别进行除法运算。其数学含义分别为 a / b = c 与 x / y = z。
- 让我们先来看看红色注释 1 标注的代码片段
do_divide(a, b)(见图 1)。我们可以通过在前面两行的BigDecimal::new()推断出a和b的类型都是BigDecimal,同样的,在注释 2 所标记的位置,我们也可以轻松地推断出x和y的类型都是I32。 - 接下来在注释 3 所标记的位置,我们可以推断出函数
do_divide()的参数dividend和divisor可能是BigDecimal或I32类型。同时在函数体内的dividend.divide(divisor)这一语句中,我们发现dividend有一个成员方法divide()。于是我们可以推断出参数dividend的类型还有可能是匿名接口interface { fn divide(this, T) -> T; }。 - 此时可以尝试使用类似前文所提到的 Structural Typing 的推断方式。我们设
A为BigDecimal,B为I32,C为推断出来的匿名接口interface { fn divide(this, T) -> T; }。如果存在一个接口x,满足x ⊆ A ∩ B,且x ⊇ C则推断出来的接口类型为x。否则,推断出来的类型为any。最终,我们在注释 4 所标记的位置找到了满足条件的Divider接口,于是我们可以将dividend和divisor的类型推断为Divider。 - 在最后,根据 Structural Typing 的规则,我们还可以将
BigDecimal和I32视为实现了Divider接口的类型,并在注释 5 和 6 所标记的位置展示了对应的 Inlay Hints。

- 静态类型推断语言配合 Inlay Hints
实际上,编译器还可以实现更多的自动推断,比如在类或接口的定义、变量参数是否为 const 或 mut 等方面加入更多的约束。这些推断结果都可以在编辑器中使用 Inlay Hints 显示出来。
然而,在重构方面有经验的朋友可能会考虑到这样一个问题:如果我们修改了类或接口的定义,可能会出现一些让人摸不着头脑的错误。例如,我们为接口 Divider 添加一个成员方法 remainder(this, divisor),此时在注释 1 和 2 的位置会出现参数类型错误的提示,因为编译器无法找到满足 BigDecimal ∩ I32 的接口定义。然而,我们实际上修改的位置却是 Divisor,这在大型项目中可能会非常致命。为了解决这个问题,一个可行的方案是使用版本控制工具(如 git)比较当前代码与上一个版本的代码的差异,并生成更为智能的错误提示。如果代码来自第三方库,我们可以使用包管理工具与 git 配合查找差异的部分。
但让我们再考虑这样一种情况:假设当前参数类型被推断为 A,但在一些版本的迭代后,A 内部发生了修改,不再满足参数类型的要求。然而,另一个接口 B 仍然满足参数类型的要求,因此项目的编译仍然能够成功。随着版本迭代的进行,参数推断的类型可能会一直变化,但仍能正常工作,如 A -> B -> C ... -> H,直到很久以后,当我们修改 H 的声明后,可能会出现莫名其妙的错误。这样的情况会让我们感到迷惑,我们甚至可能不知道这些类最初希望实现的是哪个接口。虽然我们仍然可以通过历史工具找到参数推断的最初类型,但这无疑会增加多余的工作量。如果最初我们直接使用 LSP 为参数 ”自动生成“ 推断后的类型,就不会出现那么多非预期现象。
如果我们为了使代码本身具备更强的约束,从而将大量的自动推断转换为代码生成则会出现如 图 2 的样子。这在一定程度上回归了传统的静态类型编程语言,即使使用 LSP 的重构功能,在面对大规模重构的需求下仍然非常困难。

2. 静态类型语言配合代码生成工具
因此,我们可以得出一个这样的结论:”代码生成“ 功能可以辅佐编程语言更倾向主动设计,而 “Inlay Hints” 功能可以辅佐编程语言更容易 “被动匹配”。但历史常常告诉我们,在许多情况下,做出一个折中的决定是比追求极端的解决方案更加明智的选择。因此,相信在未来,如何在这两个方面找到平衡,将成为编程语言设计面临的新挑战。
§本章小结
LSP 和 Inlay Hints 的出现对编程语言的设计带来了革命性的变化。”代码生成“ 和 ”Inlay Hints“ 都能提升代码的易用性和易读性,但本章节使用一个案例说明了二者的巨大差异。并且指出未来的编程语言需要在这两个方面进行更多的权衡,找到属于自己语言定位的平衡点。
值得一提的是,以往读代码的几乎都是人类,而在这个特殊的转折点我们可以看到 AI 也加入了 “读代码” 和 “写代码” 的活动中。因此可能在不久的将来 “AI 易用性” 或 “AI 易读性” 之类的衡量编程语言的指标可能将会腾空出世,让我们拭目以待。
§Water 编程语言的核心设计与实现
§前言
Water 的启发源自于前文所提到的 “Structural Typing 类型系统” 以及 “LSP 和 Inlay Hints”,旨在探索编程语言和开发工具之间结合设计会带来哪些可能性。
Water 是一个静态的、基于类型推断的、拥有垃圾回收功能且以字节码为基础的解释型编程语言。它支持 Static Typing 与 Structural Typing 两种类型系统,它的灵感源自于一些现代编程语言,如 Go、Rust 和 Java,以及,以在 ”代码生成“ 和 ”Inlay Hints“ 中寻找平衡(见前一章节)为基础,设计的一门编程语言。
§核心设计
§面向对象
在当今的编程语言中,大有如果不支持面向对象就不被认可的架势,因此对面向对象的支持成为编程语言设计的首要任务。传统对面向对象的定义是支持抽象、封装、继承和多态,但是其中的 “继承” 是被众人诟病最多的特性。
在早期的面向对象编程中,人们将继承视为重用代码的主要方式。而随项目规模的扩大,人们发现长链路的继承体系会使得代码难以维护和扩展,也因此在 GoF 中提出 “组合优于继承” 的设计原则,组合成为了代码复用的首选方式。而近年来许多新的编程语言已经不再将提供 “继承” 这个功能,因此在现代面向对象语言中,继承不再被视为 “必须具备” 的功能。
因此 Water 的面向对象体系中并不打算支持继承这一功能,而更提倡使用更为灵活的 “接口” 来实现抽象、封装和多态这些功能。
因为 Water 中没有继承功能,因此将关键字 class 替换为 struct,这与 Go、Rust 和 Zig 等新编程语言基本保持一致。
§类型系统
Water 语言被寄于设计成一门兼顾类型安全、易用性和易读性的高级编程语言。而需要实现类型安全,则必须为静态类型语言。而 Water 不仅支持 Static Typing 还支持 Structural Typing,这其中的缘由与前章节 “探索未来编程语言的设计” 的结论有关。
因此 Water 允许使用者显式为 struct 指定接口(或自动生成),这与 Static Typing 一致。亦允许不显式为 struct 指定接口,此时则使用 Structural Typing 类型系统,并使用 Inlay Hints 在编辑器上显示 struct 通过 Structural Typing 实现的接口。
其中允许手动指定是为了使使用者拥有主动设计的机会,这在一些项目中仍然比较有用。另外允许自动推断是为了使代码拥有更灵活和更高的复用率,这与 Go 语言的考量是一样的。
§语言服务器
如前文所说,现代的编程语言不能仅仅考虑实现一个编译器或解释器,还需要考虑如何使自己的语言与各种工具更好地配合使用。
Water 被设计成一个基于类型推断的语言,语言服务器是必不可少的功能。因此 Water 的语言服务器需要为编辑器提供代码片段中的变量类型,以及使用 Structural Typing 隐式实现的接口。
成熟的语言服务器往往需要提供更多强大的如代码补全,代码生成等高级功能。但考虑其工作量较大,因此并不是 Water 现阶段的目标。
§单元测试
单元测试是保障代码质量必不可少的工具,因此 Water 将单元测试功能也放入语言标准中。Water 内置单元测试的设计实际上是受到 Go 语言的启发,因此其使用方式与 Go 语言的单元测试非常类似。
如 图 1 所示,使用者只需要在文件中引入标准库文件 water.testing,即可使用单元测试结构体 Testing,其中 Testing 中提供了一些通用的测试工具如 assert()。
use water.testing;
fn fibonacci(n: i32) -> i32 {
if (n == 1 || n == 2) { return 1; }
return fibonacci(n - 1) + fibonacci(n - 2);
}
fn test_fibonacci(t: Testing) -> void {
t.assert(55 == fibonacci(10));
}
- Unit Testing
最后,使用者只需要通过使用命令 water test <file> 即可运行文件中的所有单元测试,也允许通过命令 water test <file> <target> 运行文件中指定的单元测试,其中 target 为单元测试的函数名。即可看到如下输出结果 图 2。
=== RUN test_fibonacci
--- PASS: test_fibonacci (0.00s)
- Unit Testing output
§运行环境
考虑到笔者对 JIT 技术存在偏爱,从而将 Water 被设计为基于字节码解释执行的语言。但事实上, JIT 技术目前暂未被实现在 Water 中。
Water 的定位是高级编程语言,因此希望其使用方式能与大多数高级编程语言一样,无需手动分配和释放内存。因此还需要实现一个垃圾收集机制。
§语法设计
§类型转换语法
在 Water 中的类型转换语法与大多数编程语言都不一样,其设计灵感来源于 Go 语言。
传统语言的类型转换通常是像这样的写法 Line line = (Line) shape。只是这种前置类型转换符的书写方式,在成员堆叠很多层时使用转换,视野必须回到左边。另外,如果希望在转型后再次引用其转型后的结果,外面必须要加上括号。因此 Water 采用后置类型转换符的语法 图 3。
// java
Line line = ( (Bazz) bar.foo() ).get();
// water
let line = bar.foo().(Bazz).get();
- 类型转换
§与 Inlay Hints 结合的语法
源于笔者对 Rust 语法存在偏爱,因此 Water 的语法大量参考自 Rust 和 Zig,而剩余部分的语法与 Java 类似。
由于在 Water 的设计之初,就开始考虑其如何与其它开发工具相结合,因此语法设计除了书写部分,还需要加上 Inlay Hints 的部分才算完整 图 3。

程序的输出结果:
Point(x=10, y=20)
Point(x=20, y=10)
Point(x=20, y=30)
Point(x=30, y=20)
Null reference.
图中的程序基本涵盖了 Water 的核心语法,为避免出现理解上的歧义,因此列出注解:
- 第 1 行的
use water.lang代表引入 Water 的标准库,而紧跟其后的 Inlay Hints 部分则是显示当前文件引用了lang文件中的Stringable接口与println函数。 - 第 3 行是
Rotatable的接口定义,其里面声明了rotate(this)成员方法,其返回值类型是any。 - 第 7 行是
Point的结构体的定义,其显式实现了接口Rotatable,其上方有一个 Inlay Hints 显示其通过 Structural Typing 推断隐式实现的接口。- 第 8,9 行是
Point的 2 个公有成员变量x和y - 第 11 行是定义公有静态方法
new()。 - 第 16 行是定义公有成员成员方法
rotate(),该方法来自显式实现的接口Rotatable,因此方法上需要标注override。 - 第 23 行是定义公有成员成员方法
to_str(),该方法来自隐式实现的接口Stringable,因此其override是 Inlay Hints。
- 第 8,9 行是
- 第 28 行是
print_obj()函数的定义,参数类型是Stringable,没有返回值。 - 第 32 行是
main()函数的定义,与大多数编程语言一样,这是程序的入口。- 第 33-37 行是
points变量的定义,其右值是一个数组字面量,里面的Point::new()表示调用Point的静态方法new()。通过其返回值类型可以推断出这是一个Point类型的数组,因此points变量的类型可以推断为Point[]。 - 第 39-48 行是异常捕获,这与 Java 类似。
- 第 40,41 行是
for循环遍历points内的元素,也与 Java 类似。 - 第 43 行是关键代码,可以看到
print_obj函数期望接收一个Stirngable类型的对象,因此Point会隐式实现接口Stringable。 - 第 44 行与 43 行类似,区别在于其使用类型转换将
point.rotate()的any转换为Point。
- 第 33-37 行是
§核心功能的实现
§技术概述
Water 的主要实现语言为 C11,编译工具使用 CMake。
由于笔者希望更专注语言设计本身,因此并没有考虑自己编写词法分析器,从而选择使用现成的工具。其中词法分析器采用 flex,解析器使用 bison,因此只需要编写分词规则和符合 BNF 规范的语法规则。
而 Water 目前已具备一些基本的标准库功能,这部分的功能是使用 Water 语言编写完成的,如容器 HashMap。
在 Water 的开发中,参考过一些编程语言的实现,其中有大量的代码来自于 Diksam 语言[*16]。
§类型系统与类型推断
Water 与大多数语言不一样的地方是它支持两种类型系统,它即支持传统的静态类型语法,也支持更为灵活的 Structural Typing。
其中静态类型语法方面,和多数静态类型语言的实现方式基本一致,都是在语义分析阶段对变量进行静态校验,但 Water 还需要在此基础上提供类型推断功能。
变量类型推断功能的实现并不困难,因为只需要先计算出左值或者右值的类型,既可推断出另一方的类型,仅会出现以下 4 种情况(图 4)。
let a = 1.2; // 1.2 (f64)
let b: i32 = 1.2; // 1 (i32)
let c: i32 = "1.2"; // error
let d; // error
- 类型推断 4 种情况
而对于 Structural Typing 的推断,通常有两种实现方案。
- 方案一是在静态检查开始前,遍历所有结构和接口,通过排列组合计算出结构实现的所有接口。
- 方案二则是在进行静态检查的过程中,当需要判断变量是否属于某接口时,再对其结构和接口做匹配校验。
显然方案二的性能会更高,而方案一在语言服务器中使用可以为使用者提供更全面的提示,但 Water 的 Structural Typing 目前仅使用方案二实现。
§语言服务器
Water 编译器前端的代码同时也被复用在语言服务器的功能上,这参考自 rust-analyzer 的设计。
目前 Water 的语言服务器仅支持查询变量名被推断后的类型,以及结构体显式或隐式所实现的接口列表。如 图 5 所示,这是使用命令 water lsp dump <file> 的输出结果,该代码文件使用 图 1 的程序。
+----------------------+
| STRUCT DEFINITIONS |
+----------------------+
Rotatable(:5)
Point(:9)
| Rotatable
| Stringable
+----------------------+
| FUNCTION DEFINITIONS |
+----------------------+
rotate(:5)
new(:14)
| x: i32
| y: i32
rotate(:22)
| x: i32
to_str(:26)
print_obj(:31)
| o: Stringable
main(:46)
| points: Point[]
| i: i32
| point: Point
| e: Exception
- LSP dump
实际上通过对编译器前端代码的复用,在静态检查阶段完成类型推断后,获取程序中的实际信息是非常容易的。
§单元测试
Water 内置单元测试的实现方式仍来自 Go 语言的启发。而实现内置单元测试实际上只需要实现两部分的功能。
第一部分是使用 Water 语言本身编写出单元测试标准库 water.lang 以及测试结构体 Testing。
在第二部分中,可以将问题简化为 “按规则检索函数” 以及 “自动运行指定的函数” 两个问题,函数的信息在编译器前端完成静态检查后能够轻易获得。
因此只需要将函数参数是 Testing 类型的函数筛选出来,并在最后的字节码生成阶段生成调用这些函数的字节码即可实现该功能。
§垃圾回收机制
Water 提供垃圾收集器,因此使用者可以像使用大多数高级编程语言一样,无需手动分配或释放内存,垃圾收集器会自动帮使用者完成这件事。
但目前 Water 的垃圾回收算法非常简易,并没有自己管理堆区域,每次分配空间都会调用 malloc 函数,并将分配的空间用链表连接起来。
每当字节码执行到安全点时,垃圾回收算法则会从栈上的对象开始进行可达性分析,并将可达的对象标记起来。最后,遍历整个链表,并使用 free() 释放不可达的对象,以实现自动垃圾回收的功能。
§总结
本文的主要目的是探索未来编程语言的设计,并提出未来编程语言设计还需要在 ”代码生成“ 和 ”Inlay Hints“ 之间寻找平衡的观点。其中研究的部分主要包含以下两部分。
在对 “Structural Typing” 的研究中,发现主流的动态类型语言和静态类型语言正在相互汲取对方的优点,甚至许多动态类型语言有转化为静态类型的倾向。以及越来越多现代编程语言也在不同程度上支持 Structural Typing 这种结合动态和静态优势的类型系统。
在对 “LSP 和 Inlay Hints” 的研究中,发现近年来越来越多编程语言的设计受到开发工具的影响。以及发现在 “Inlay Hints” 的出现解决了静态类型语言 “滥用类型推断语法可能会影响代码的可读性“ 的问题。因此提出如下观点,现代编程语言的设计者们不仅仅需要设计一门语言,还必须考虑如何使自己的语言与各种工具更好地配合使用。
最后,根据研究的结果以及笔者的主观设计,尝试将部分功能在 Water 语言中实现,目的是验证其设计的可行性。
§参考文献
[1] Language Server Protocol. https://microsoft.github.io/language-server-protocol
[2] Inlay-Hints. https://rust-analyzer.github.io/manual.html#inlay-hints
[3] A Large Scale Study of Programming Languages and Code Quality in Github.(2014). https://web.cs.ucdavis.edu/~filkov/papers/lang_github.pdf
[4] Why Create TypeScript. https://www.typescriptlang.org/why-create-typescript
[5] MyPy. https://mypy-lang.org
[6] PEP 484 – Type Hints. https://peps.python.org/pep-0484
[7] Decltype and auto. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1478.pdf
[8] Almost Always Auto. https://herbsutter.com/2013/08/12/gotw-94-solution-aaa-style-almost-always-auto
[9] Duck Typing. https://en.wikipedia.org/wiki/Duck_typing
[10] Type Compatibility. https://www.typescriptlang.org/docs/handbook/type-compatibility.html
[11] Effective Go #Interfaces. https://go.dev/doc/effective_go#interfaces
[12] The Go Programming Language. https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=6898707
[13] PEP 544 - Protocols: Structural subtyping (static duck typing). https://peps.python.org/pep-0544
[14] C++ extensions for Concepts. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0734r0.pdf
[15] Add support for virtual text or padding in textprop. https://github.com/vim/vim/issues/7553
[16] プログラミング言語を作る(中文版:《自制编程语言》). http://kmaebashi.com/programmer/devlang/book/index.html