本文是对 Austral 编程语言创建者 Fernando Borretti 的一次访谈,探讨了 Austral 的设计理念、线性类型系统的应用、以及与其他编程语言(如 Rust、Haskell、OCaml)的对比。Fernando 阐述了创建 Austral 的动机,以及它在内存安全、资源管理和并发处理方面的优势与考虑。
我们已经很久没有采访过语言的创建者了,所以我们非常兴奋地向 Austral (https://austral-lang.org/) (Github) 语言的创建者 Fernando Borretti 提出问题并分享他的答案。正如其网站上所说:
“Austral 是一种新的系统编程语言。它使用线性类型来提供内存安全和基于能力的安全性代码,并且被设计得足够简单,可以被一个人理解,重点是可读性、可维护性和模块化。”
正如 Pascal 向一代程序员介绍了模块,Lisp 介绍了垃圾回收一样;Rust 将使用类型系统来强制执行资源使用规则 引入了主流 。
它引发了一场非常有趣且持续的关于内存使用、资源处理和线性类型系统的讨论,这些讨论正在启发许多其他语言。我们 Lambda 的成员希望将来也能提出我们自己对此的见解。
事不宜迟,以下是采访内容。
你为什么创建 Austral?Rust 不是解决了相同类型的问题吗?
我认为 Manuel Simoni 曾说过:编程语言最重要的事情是它给你的感觉。
对很多人来说,这听起来像个玩笑,但我非常认真地对待它。编程语言设计是一种情感的事情。我停止使用 Python,因为它让我感觉自己总是站在强风中的纸牌屋顶上。它让我感到焦虑。JavaScript 也非常相似。
对于编程语言来说,有一种类似于生物学中延伸的表现型:除了核心语言和标准库之外,你还有“延伸的语言”、工具、生态系统、社区、文化。所有这些东西结合在一起,定义了你对这门语言的体验。有些语言(如 OCaml)具有很多技术优点,但工具很糟糕,社区也没有兴趣改进,因此你坚持使用它的技术美感,然后不可避免地会感到疲惫。而且你离核心语言越远,控制就越少(很难对整个语言社区进行社会工程),但是语言创建者可以控制很多事情,例如设定社区的基调、对文档的期望、工具的质量。
我想要一种(以及一种扩展的语言)让我乐于使用的语言。我想要一种小而简单的语言。简单是指柯尔莫哥洛夫复杂度:它可以装在你的脑海里,并且没有大量你需要理解的极端情况。我想要一种发展缓慢、保守的语言,本着 Common Lisp 的精神,代码的位腐烂非常非常慢,你可以放心地编写今天的代码,并且知道它在三十年或更长时间后仍然可以编译和运行。我希望构建一个扩展的语言来支持它:高质量的工具和高质量的文档来设定基调,并创建一个人们重视质量、品味和工艺的社区。
关于 Rust,我非常喜欢 Rust。我在工作中用到它。这些工具用起来很愉快(在被 pip 和 dune 以及几乎所有其他东西折磨多年之后)。而且它的设计比你能找到的大多数其他语言都要好得多。我什至会为 async 辩护。
但是 Rust 是一种非常务实的语言,而务实的问题在于它永远不会结束*。务实没有自然的停止点。Rust 已经非常复杂了,而且我预计它会随着人们对语言提出更多要求而继续增长。关于编程语言的事情是你无法真正删除功能。这不一定是错误的:我认为如果 Rust 没有一千个小的人体工程学功能,它就不会如此成功,而且如果它没有 async,那么采用它来构建服务器的动力就会小得多。
构建通用语言有两种方法:一种是使其不专门用于任何一件事,这就是 Austral 的方法;另一种是使其专门用于每一件事。而且事情往往会朝着后者发展,因为大公司——那些员工坐在编程语言基金会董事会中的公司,以及那些付钱让人开发编译器和工具等的公司——有非常具体的需求,而且他们总是在游说让语言解决他们的特定问题。因此,语言不断增长并积累所有这些功能,因为 Google 需要将全球延迟降低 0.02%。
*Philip K. Dick 最早这样评价内省,他是对的。
哪些语言对你的启发最大?
Rust 获得了很大的赞誉,因为它是有任何类似线性类型的唯一工业语言。
Cyclone 也启发了 Rust,是一种研究语言,是 C 的一种更好的方言,但并没有流行起来,但他们发表了一些关于它的论文。关于基于区域的内存管理,那里有非常有趣的想法。
Haskell 的类型类做得很好。特别是 Haskell 98 类型类是优秀设计的瑰宝。Standard ML 的模块系统。Ada 的语法、模块系统以及关于安全的想法。
什么是线性类型系统,它有什么用?你认为哪种类型的软件可以通过使用线性类型系统来改进?
我已经在不同的地方写了一些关于这个的文章:
https://borretti.me/article/type-systems-memory-safety
https://borretti.me/article/how-australs-linear-type-checker-works
https://borretti.me/article/introducing-austral
https://austral-lang.org/linear-types
我的一部分想把这些合并成一个“权威”的解释,但另一部分认为对同一个想法有不同的方法是有价值的。所以我有很多不同的电梯演讲:
一种思考方式是,线性类型允许你在编译时强制执行协议。编程中有两种值:普通数据和协议Handle。后者包括套接字、文件对象、数据库Handle、IPC 通道。在使用手动内存管理的语言中,它们包括堆分配的对象。
这些必须符合特定的协议,具有正确的状态转换。没有 double-free(你不能释放两次内存)和没有 use-after-free。线性类型允许你在编译时强制执行此操作。这是主要的优点:你可以获得高性能的手动内存管理,而没有安全隐患。
但是你也可以为你自己的类型创建你自己的协议,并强制执行比普通类型系统允许的更高级别的 API 契约。
另一种思考方式是,线性类型使值像现实世界中的对象一样工作。在现实中,事物只能在一个地方。它们移动,但不能复制。在计算机中,复制是原始操作。值可以被别名,因为指针不受限制。
事实证明,突变的很多问题实际上是别名的问题。当你通过线性类型限制指针别名时,你会在普遍突变的情况下获得引用透明性。你获得易于推理且性能非常高的代码。
至于哪些类型的软件可以改进:主要是任何手动管理内存或使用需要遵守协议的外部资源的软件。这是主要的改进。但是当你开始考虑从头开始使用线性类型设计 API 时,它会变得更加通用,因为通过使用线性类型来强制执行高级契约和协议,可以改进很多 API。
使用线性类型系统有什么缺点?你认为开发者体验或学习曲线是否一定会受到影响?
主要有两个缺点:
你解释线性检查器的文章详细介绍了实现。一些现代语言正在探索在逻辑推理引擎(例如 Datalog)中将它们的类型系统实现为规则集。你对此趋势有什么看法?
我对逻辑编程的了解不够,无法在其中实现类型检查器。我了解这个名为 Redex 的 Racket 工具,但没有使用过,它基本上允许你用 Gentzen 表示法(如 PLT 论文)编写类型判断,但可以让这些判断进行类型检查。这比用 LaTeX 编写类型系统有了巨大的改进。
另一件事是类型系统不是太复杂。目标是在 C. A. R. Hoare 的意义上是简单的,即“足够简单,以至于显然没有错误”。
增量编译也是当今的热门话题。在你解释 Austral 编译器的设计的文章中,你提到为了简单起见,它进行批量编译。你是否认为增量编译是一个有趣的功能,还是你认为它只是一个实现细节?
增量编译和单独编译是生产编译器的必备功能,但我认为在早期你可以没有它们,特别是由于一开始用该语言编写的代码并不多。你可以将整个生态系统增加 10 倍的量,仍然不会受到编译速度慢的影响。
我认为这是一个相对于 Rust 等其他语言有改进空间的地方,因为在 Austral 中,模块是编译单元,而在 Rust 中,crate 是编译单元。在 Rust 中,组成一个 crate 的所有模块都会一次性加载,然后才进行编译,因此你可以在一个 crate 中存在模块之间的循环依赖关系。问题是构建时间是人们对 Rust 的主要抱怨,而且人们不得不求助于不良解决方案,例如手动将代码库拆分为多个 crate。
在你的 Austral 介绍中,你提到类型推断是一种反特性。你能详细说明是什么导致你做出这个决定吗?
我觉得类型推断是一个打破笼子并逃离实验室的科学实验,给很多人带来了损害。也就是说,它应该仍然是一种学术上的好奇心。
根本问题在于类型推断不知道你给它的输入是正确的还是错误的,但无论如何它都会将其用作推断中的约束。在 OCaml 中我经常遇到这个问题:我会犯一个错误,在 Java 中我会收到一条错误消息说“你犯了一个错误”,而在 OCaml 中,编译器会尽最大努力进行推断,将我的错误向上、向下和到处传播,然后我会收到一个难以理解的类型错误,有时与我犯实际错误的地方相隔数十行或数百行。有时解决此类错误的唯一方法是开始添加类型注释(到函数签名、到变量)以约束推断过程,这可能需要很长时间。然后你找到了错误,发现这是最琐碎的事情,而在一种不太复杂的语言中,这种情况一开始就不会发生。
下一个问题是推断太多的语言。同样,在 OCaml 中(不像 Rust),你可以让函数的参数不带注释。你可以节省几微秒的输入时间,并且在该代码库的剩余生命周期内,你将花费几分钟的时间来弄清楚某事的类型是什么。你可以说,好吧,只需注释所有函数签名。但这就是为什么语言必须有硬性规则:如果某件事是可选的,人们会走捷径并且不会一直这样做。
因此,ML 系列语言中的类型推断是一个失败的想法,因为你最终还是会注释类型:你必须注释函数的类型以进行文档编制,并且你经常最终注释局部变量的类型,以便提高可读性并约束类型推断引擎并使错误更容易。这只是一种非常令人沮丧、迂回的方式来做在 Java 中你一开始就被迫要做的事情。我看到人们使用带有 LSP 设置的 VS Code 来显示代码中变量的类型,并认为,好吧,为什么不直接写出来呢?然后你可以在你的开发环境之外阅读代码,例如在 GitHub 差异中。
我发现类型推断仅在一组非常狭窄的情况下有用,在这些情况下,类型信息不会严格向下流动,并且注释会很麻烦。这方面最好的例子是 Option 类型。如果在 Rust 中有以下内容:
enum Option<T> {
Some(T),
None,
}
然后在 Some 构造函数中,不需要推断,因为类型信息向下流动:Some: T -> Option<T>。但是如果没有类型推断,None 构造函数会更难:它不接受一个值,所以在一种没有类型推断的语言中,你必须告诉编译器应该使用哪个类型来代替 T。但是对于这样一个狭窄的用例来说,通用的类型推断引擎是一个非常复杂的机器。
然后是性能成本。类型系统越高级,推断就越昂贵。还有类型推断浪费了大量学术努力的事实。PLT 上的学术论文会介绍一种新的类型系统,然后花费大量的篇幅来描述类型重建算法。我想说,这是最没有意思的部分!让我手动注释事物,并向我展示这东西可以做什么来解决实际问题!
因此,在 Austral 中,类型信息仅在一个方向上流动,并且变量和所有内容都需要在所有地方进行注释。代价是你花费了无法观察到的额外毫秒来编写。收获是代码立即变得更具可读性,并且你再也不必处理奇怪的推断错误。
宏也被提到是一种反特性,但在你的文章中你提到了 Lisp。你认为在一般情况下或在 Austral 中,元编程是否存在有效的用例,以及哪种类型的元编程?
我过去写了很多 Common Lisp。而且宏在 CL 中运行得还不错*。吸引我使用 Lisp 的一件事是每个程序员都是语言设计师。我曾经认为这是一件非常好的事情:你可以在几秒钟内实现语言特性,而 Java 程序员已经在邮件列表中恳求了多年。但是后来我看到了人们用宏做什么,然后改变了我的想法。
这是我年轻时想要表达能力的一般模式的一部分,我被 Common Lisp 吸引,因为在 Common Lisp 中你可以挥舞你的魔杖并改变世界。但是在清理了人们 10 年的可怕代码之后,我意识到我想要的是更少的噩梦。宏使每个人都成为语言设计师,而这我意识到是一件非常糟糕的事情,因为大多数人不应该靠近语言设计。宏可能在一种仅供疯狂的 PL 天才使用(他们还具有出色的沟通技巧并编写了大量文档)的语言中有效,但是“此功能只能由具有出色品味的具有辨别力的天才使用”在现实世界中是不可持续的。
人们使用宏系统(以及相关的东西,例如 Java 风格的注释)做什么?主要是为了构建噩梦:代码库中充满了魔力,其中每个类型都有大约七个不同的与序列化、RPC、SQL 映射等相关的注释。你在页面上看到的代码不是正在运行的代码:它是输入到一个庞大的、定义不明确的、特设的编程语言中,该语言由以不可预测的方式转换代码的第三方宏组成。错误变得不可能追踪,因为没有人可以具体地告诉你控制流是什么样的。对代码库的更改变得不可预测。
因此,宏有点像是诱饵和转换。诱饵是,“如果有一种简写方式来编写这段公共代码就好了”。转换是你最终得到一个没人能理解的代码库。
解决方案是构建时代码生成。它很像宏,但是你可以检查生成的代码,将其提交到版本控制,对其进行调试,并且它与你编写的其余代码干净地分开。
基于能力的安全性描述听起来与 OpenBSD 的 pledge 非常相似。你是从中获得灵感的吗?
这是我希望在迭代语言设计时保留像实验室笔记本一样的东西的一个领域。能够回去看看我意识到了什么以及何时、我读了哪些论文等等将是无价的。我认为那时我意识到 pledge 以及它的工作方式。我真的很喜欢 pledge API。与 pledge 的基本简洁性相比,Linux 和 FreeBSD 的能力非常复杂。Austral 基于能力的安全性与 pledge 类似,因为在这两个系统中,你都从无限的能力开始,然后你可以一次一个地不可逆转地放弃这些能力。但是 Austral 的系统更精细,因为它不依赖于硬编码的 syscall 列表,而是,你可以在值级别获得 pledge(),你可以 pledge 单个文件和其他对象。
设计像 Austral 这样的新编程语言最困难的部分是什么?
我应该说建立一个社区,让人们感兴趣,但老实说,最令人沮丧的事情是编写编译器。
一方面,你想要最简单、最 MVP、最原型引导的编译器,这样你就可以进入编写真正运行的程序并实际开始使用该语言的阶段。这可以告诉你很多关于人体工程学、关于可能的可靠性问题的信息。因为当事情模糊不清时,它们总是很棒的,只有当你具体化事物(通过实现它们)时,你才会开始注意到缺陷和权衡。
但是如果编译器太 MVP,你将遇到无法轻易解决的错误,例如,由于错误报告非常差。编译器在测试和调试方面确实非常困难。
因此,你总是在“构建一个简单的 MVP 编译器,以便我可以快速地对其进行迭代”和“构建具有生产级诊断和错误报告的东西”之间不断改变方向。
你打算建立一个社区或用户群吗?你认为你如何才能产生吸引 Rust 或 C 程序员使用 Austral 进行开发的动力?
我有一个小型的 Discord。我想做更多的工作,以拥有更实质性的东西,特别是围绕标准库和构建工具,然后再在营销上花费更多的精力。我认为很多程序员都厌倦了语言的快速变化、框架的快速变化和库的快速变化,而小型、简单、保守、发展缓慢的语言的想法很有吸引力。这是你可以在一个下午学习的东西,并且你编写的代码将在 30 年后编译并运行,而且你不必在十年后惊恐地逃离。
你认为你可以重用其他语言的现有工具(如 gdb 或 rust-analyzer)吗?标准库的状态如何,你如何看待它的发展?
当前的编译器输出 C。我不希望这成为该语言的特征(“Austral 编译为 C”),因为它只是编译器的实现细节。因此,gdb 和 valgrind 应该是可用的。
rust-analyzer,我对此表示怀疑。它是一个巨大的东西,并且本质上是专门为 Rust 设计的编译器前端中最复杂的部分。
我认为编写生产编译器时要考虑到使其也可以用作 LSP 是一个好主意。
标准库非常小:简单的容器和类型类。我希望自己对其进行小的补充。很多人讨厌依赖关系,但我非常相信很多小型库,实际上我喜欢标准库只是“永恒”(例如,可调整大小的数组类型)或普遍存在的(例如,日期和时间)或绑定一些特定于平台的东西(例如,文件 I/O)的代码的想法。
与其他语言(例如 FFI)的互操作性是否是路线图的一部分?它将如何与线性类型和能力互动?
与 C 的互操作性已经存在。这是最有用的一个,因为 C 调用约定基本上是每种语言的通用语。
有些语言宣传例如与 C++ 的自动互操作性。这需要付出更大的努力,而且我认为这完全是错误的。例如,Common Lisp 的 Clasp 编译器基本上是为了作者可以从 Common Lisp 访问使用模板等的 C++ 库而构建的。当你只需要在你需要的 C++ 代码周围编写一个轻量级的 extern C 包装器时,这会付出巨大的努力(在 Common Lisp 中,你甚至可以自动化其中的大部分)。因此,我不太担心 C++ 互操作性。将来,我们将使用 LLM 移植整个 C++ 代码库,没有任何问题。
你对 Austral 的未来计划是什么?你计划扩展该语言并添加新的功能(如并发原语)吗?
标准库、构建系统和包管理器、更好的文档。这是第一件事。
我一直在拖延并发模型,因为我对它们了解不够,而且我不想过早地将该语言专门用于一种可能无法实现的方法。Go 有绿色线程和 goroutine,但这对他们来说并没有奏效,这种设计放弃了很多性能。OCaml 现在有绿色线程,而且到目前为止似乎对他们来说效果很好。我认为 Rust 风格的 async 受到了非常不公正的诋毁,但它也存在实际问题,因为由于它与生命周期交互的方式,每个人最终都会将他们所有的共享资源放在引用计数指针下。因此,从理论上讲,性能上限非常高,但在实践中,人们会放弃很多性能来获得可以实际编写和重构的代码。
因此,我很高兴坐下来让世界为我定义自己,并且当有一个明确且引人注目的正确做法时,我将以最简单、最正交的方式在 Austral 中实现它。
如果你喜欢对编程语言创建者的采访,你可能也会喜欢以下以前的采访:
- 原文链接: blog.lambdaclass.com/aus...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!