一年多前,我写了一些关于“更小的锈”的笔记,这是一种更高级的语言,它将从Rust的一些类型系统创新中获得灵感,但由于针对的领域对用户控制和性能的要求不那么严格,所以会更简单。在今年失业期间,我致力于更详细地勾勒出这样的语言会是什么样子。我想写一点关于在这段时间里我得出了什么新的结论。
重读我的上一篇文章,我惊讶地发现我对这种语言的目的声明是如此含糊。我的整篇博客文章实际上都集中在区分这种语言和Rust,我的讨论框架是关于我将从Rust中删除什么,以及这种语言如何不支持Rust的某些用例。这并不令人惊讶:我当时正在研究铁锈,我从来没有像现在这样花时间去思考这种假设的语言本身。
这个设计的目标是创造一种能够与“应用程序语言”竞争的语言。该语言的设计目标是:
它不应该特别难学。在可能的范围内,它应该是大多数程序员所熟悉的。由于我的练习是尝试将所有权和借用权应用于应用程序域,因此它必然包含大多数程序员认为非常新颖的一些特性(如Rust的“生命周期”)。但总的来说,我们会尽量减少入职门槛,使事情简单化。
它应该能快速检查类型并进行编译。它不应该有糟糕的批处理编译性能,而且它的设计应该考虑到增量重新编译,以便为将编译器集成到开发环境中的用户提供良好的体验(使用完整的IDE,甚至只使用文本编辑器的插件)。我在上一篇文章中甚至没有提到这个问题,正如其他人在其他地方讨论的那样,Rust糟糕的编译时间不是它先进的类型系统的结果,而是其他因素的组合。有些是必不可少的,比如它提供的运行时保证(例如单形化),而另一些则是偶然的,比如它的模块系统的某些方面。这些因素对我们的语言来说都不是必不可少的,所以我们要小心避免这些陷阱。
它应该有一个运行时,能够很好地适应当今应用程序语言的主要用例。这意味着主要是非常适合于Web开发,前端和后端都是如此。(不幸的是,对于不是由这些平台开发人员赞助的语言来说,很好地适应移动平台是不现实的。)。很好地适应CLI也是有益的。
我想把这篇文章的其余部分集中在我对演进Rust的所有权和借阅系统的想法上,但在此之前,我想简单地谈一下这个思考过程中的其他设计决策:
我的目标是这种语言的WASM,而且只有WASM。带有引用类型的WASM适合作为应用程序编程的环境(带有垫片,用于将来的扩展,如正确集成的垃圾收集)。这样,语言设计者就可以利用许多公司正在做的工作,将WASM建立为一个良好的共享VM平台,而不是负责平台兼容性或使用非常慢的LLVM。以WASM为目标也意味着更容易将FFI集成到与WASM在同一VM上运行的其他语言;也就是说,其他以WASM(如Rust)和JavaScript为目标的语言。
我将探索控制流捕获闭包作为核心语言抽象,类似于Kotlin。正如我在早先的一篇博客文章中所写的,灵感来自于这种假设语言的设计,我认为这是将效果与高阶函数抽象很好地集成在一起的好方法。
我会提供RESULT和OPTION的语法糖,作为处理NULL和错误的方法,类似于SWIFT。
正如我在上一篇博客文章中所写的那样,我将提供绿色线程作为唯一的并发模型,使用语言或标准库提供的通道和单元(稍后讨论)作为线程之间共享数据的方式。这些绿色线程如何映射到CPU取决于您选择在其中运行编译后的WASM的运行时间。
我没有达到设计多态性系统的地步;我可能会从比较Rust的特性和Go的接口开始,然后(了解该语言的其他特性)试着找出Rust的特性中哪些是不重要的。
我希望语言可以避免宏,因为宏(在基于模式的宏的情况下)为高级用户需要理解的语言增加了第二个元语言,而且在所有情况下都会使编译变得非常复杂。
但现在谈到这篇文章的核心:所有权和借款模式。在我之前的帖子中,我提出了一些令人疯狂的观点,我仍然基本上同意这些观点,但可能会重新定义。我是这样写的:
Rust之所以起作用,是因为它使用户能够以命令式编程风格编写代码,这是大多数用户熟悉的主流编程风格,同时在一定程度上避免了命令式编程臭名昭著的错误。正如我曾经说过的,纯函数式编程是一个巧妙的技巧,可以展示您可以在没有突变的情况下编写代码,但是Rust是一个更聪明的技巧,可以展示您可以只有突变。
资源获取就是初始化:对象应该管理像filedescriptor和套接字这样的概念性资源,并拥有在对象超出作用域时清理资源状态的析构函数。相信析构函数会在对象超出作用域时运行,这应该是微不足道的。这需要大部分的所有权、搬家和借款。
Aliasable XOR可变的:默认情况下,值只有在没有实例化的情况下才能发生变异,并且应该没有办法引入不同步的别名变异。但是,该语言应该支持变异值。要做到这一点,唯一的方法是剩余的所有权和借款,借款和可变借款之间的区别,以及它们之间的别名规则。
换句话说,Rust的核心,通常被识别的“硬部分”-所有权和借用-本质上适用于任何使检查命令性程序的正确性的尝试。因此,试图摆脱它将错过铁锈的真正洞察力,而不是建立在铁锈已经铺设的基础上。
我仍然认为这是拉斯特的“秘诀”,它确实是我所说的意思:语言必须有所有权和借用权。但我后来意识到,在用户需要这些语义的情况和它们主要妨碍用户的情况之间有一个非常重要的区别。在这之后,我意识到,在用户需要这些语义的情况和在很大程度上阻碍这些语义的情况之间,有一个非常重要的区别。这种区别存在于表示资源的类型和表示数据的类型之间。
在这个心智模型中,资源是代表“一个事物”的类型--具有身份和状态的东西,可以随着程序的执行而随时间改变。在Rust中,几乎所有东西都是资源:字符串是资源,HashMap是资源,大多数用户类型都是资源。相反,数据类型只是“信息”--事实没有意义的标识,不包含随时间演变的状态,等等。在Rust中,整数、&;str等类型都实现了复制,它们都是数据类型。(但是,对这些类型的可变引用是一种资源:稍后详述。)。
在Rust中,只有可以通过内存克隆的类型才能实现复制。这是因为Rust旨在鼓励将所有堆内存视为资源,最终用户可以通过选择何时删除表示该内存的类型来控制对该资源的管理。这在Rust的目标领域非常有价值。然而,对于大多数程序员编写的更高级别的应用程序来说,对堆内存的控制通常并不重要。这就是用户想要“关闭借阅检查器”时的意思-他们想让垃圾收集器在释放这一位数据时为他们弄清楚,因为对他们来说,这只是“数据”,而不是资源。
这种假设性的语言将倾向于这种区别。使用持久数据结构(如Clojure中的数据结构)和垃圾收集,可以被视为数据类型的类型集在该语言中不会受到限制。字符串类型将是数据类型,而不是资源;动态调整大小的数据类型数组也将是数据类型,具有作为数据类型的键和值的映射也是数据类型。
同时,表示IO对象的类型始终是资源类型。包含资源类型的集合也将是资源类型。包含资源类型的复合类型(如结构和枚举)也必须是资源类型。还有一种将数据类型转换为完全拥有的资源类型的简单方法;对于持久数据结构,将数据类型转换为资源类型将是“写入时复制”操作发生的点。因此,用户可以将所有权语义用于影响全局和外部状态(如IO)的事情,以及他们知道这将是重要的性能优化的情况。
语言处理数据和资源的方式与Rust处理复制类型和非复制类型的方式的不同是相同的。只有资源才具有仿射的“所有权”语义-在这种语义中,移动它们会使先前的绑定无效。数据类型将具有用户在大多数语言中熟悉的标准非线性语义。这意味着使用数据类型编写算法在功能上与用其他命令式语言编写算法相同,简化了用户使用该语言的过程,并将他们与线性类型相关的错误限制在他们肯定关心的区域。
前面的讨论涵盖了所有权的基础,但借款又如何呢?事实证明,资源和数据之间的这种区别也是Rust中两种引用类型之间的区别。共享引用实现复制,并且被正确地理解为“数据”(具有其自身的无意义的标识),而独占/可变引用不实现复制,并且将其视为“数据”没有意义-它是独占的(意味着它具有标识)并且它是可变的(意味着它具有更新状态)。
这意味着这两个引用类型将用作数据类型或资源等另一类型的临时视图。底层类型是数据还是资源并不重要;任何类型的“数据视图/共享引用”都是数据,任何类型的“资源视图/可变引用”都是资源。这允许用户根据需要临时切换特定值的模态。当然,就像在Rust中一样,“数据视图”不会让Fullpower超越“资源视图”所具有的类型,而“资源视图”总是可以降级为“数据视图”。(这与Rust中的引用具有相同的语义。)。
还要注意,我说的是“视图”而不是“引用”,因为该语言的设计目的是不保证类型的表示。根据对实现最有意义的是什么,要么所有类型都是“引用类型”,除非编译器可以将它们拆箱;要么如果编译器确定需要的话,所有类型都可以自动装箱。因此,这些视图不应被想象为“指向”底层类型的“指针”,它们可能与该类型具有相同的表示形式。
我之前说过,该语言将有两个用于并行子进程之间通信的原语:通道和细胞。关于通道,我没有什么有趣的东西要说,但我想讨论的是Cell类型,这是该语言唯一的共享状态原语。
单元类型将实现为垃圾收集读/写锁。这个锁的实现方式很简单,这是运行库的事情(例如,在并行运行绿线程的运行库上,它将使用原子,而在单线程上运行绿线程的运行库上,它不需要这样做。)。单元具有数据语义,但允许构造底层类型的资源视图(本质上是执行写锁定)。因此,Cell类型允许将资源类型视为数据,即使在调用资源视图方法时也是如此。它实质上是将资源类型的编译时检查移到运行时,同时删除了有关何时销毁类型的任何保证。
请注意,该语言没有Send和Sync特性,因为所有类型都有Sendand Sync语义:所有内容都可以在所有绿色线程之间共享。因此,对于可以放入细胞类型中的内容没有任何限制。
理想情况下,Cell类型甚至可以分发一个无保护的“资源视图”,而不是Rust使用的MutexGuard这样的新类型;如果编译器可以在资源视图超出范围时以某种方式插入解锁,那就太好了。不过,这可能需要一些类似于单元化的东西,因此可能会影响编译时间和实现复杂性;这可能是我们不得不离开的奢侈品。
我没有任何打算把这些想法发展成一种真正的语言,所以我认为发布我的设计工作的结果将是给我的工作带来一点影响的最好方式。我希望任何考虑设计一种新的应用程序语言的人都会考虑将这些想法作为一种方式来保证用户在资源管理方面的正确性。我愿意听取对这些想法感兴趣的人的意见;不过,就像我收到的大多数电子邮件一样,我可能会遗憾地没有回复。
由于这项设计工作与铁锈的目标是直接矛盾的,所以很难看出它对铁锈的设计有很大的影响。不过,我要提到的一件事是,自动克隆特征(具有非仿射语义的类型,尽管它们的克隆需要代码执行)的讨论与我在资源和数据之间的这种区分相关。我不认为大多数类型应该是Autoclone(我的列表可能包括:RC、Arc,也许还包括像Bodil Stokke的im这样的持久集合类型),但我确实认为,当记账必须控制管理与他们的最终目标相冲突时,选择不将内存视为一种资源会对用户有所裨益。
最后,我认为如果有人要追求这一点,我所提到的一个很大的领域是“持久数据结构”部分。我讨论了数据类型和资源类型之间的转换,有时可能需要进行新的研究来创建集合类型,这些集合类型有时可能具有持久集合的性能演算(复制成本较低,更改成本较高),有时可能具有可变集合的性能演算(更改成本较低,复制成本较高)。