引言
大约 9 个月前 我们宣布 成立了关键字泛型倡议组;这是一个在语言团队下工作的小组,旨在解决 函数着色问题 1 通过类型系统,不仅适用于 async
,还适用于 const
以及所有当前和未来的函数修饰符关键字。
我们很高兴地分享,在过去的几个月里,我们取得了很大进展,并且我们终于准备好通过 RFC 来提出我们的一些设计方案。由于距离上次更新已经有一段时间了,而且我们也很激动地分享我们的工作进展,在这篇文章中,我们将介绍一些我们计划提出的内容。
一个 async 示例
在我们的 之前的文章中 我们引入了占位符 async<A>
语法来描述“在其异步性上泛化的函数”的概念。我们一直知道我们想要一些感觉比这更轻量级的东西,所以在我们当前的设计中,我们选择放弃最终用户语法中的泛型参数概念,转而选择了 ?async
符号。我们从 trait 系统中借用了这一点,例如 + ?Sized
表示某物可能实现或可能不实现 Sized
trait。类似地,?async
意味着一个函数可能异步或可能不异步。我们也将这些称为“可能异步”函数。
是时候举个例子了。假设我们以 Read
trait 和 read_to_string 方法为例。在标准库中,它们的实现目前看起来有点像这样
/// Read from a reader into a string.
那么,如果我们将来想让这些变成异步的怎么办?使用 ?async
符号,我们可以将它们改成这样
trait ?async Read
/// Read from a reader into a string.
?async
这种工作方式是,Read
和 read_to_string
将在其“异步性”上泛化。当为 async
上下文编译时,它们将异步执行。当在非异步上下文编译时,它们将同步执行。read_to_to_string
函数体中的 .await
是必要的,以便在函数被编译为异步时标记取消点;但当不是异步时,它本质上将成为一个空操作 2
// `read_to_string` is inferred to be `!async` because
// we didn't `.await` it, nor expected a future of any kind.
// `read_to_string` is inferred to be `async` because
// we `.await`ed it.
async
我们预计 ?async
符号对于那些没有做任何特别针对 async Rust 的事情的库代码最有用。比如:标准库的大部分,以及解析器、编码器和驱动程序等生态系统库。我们预计大多数应用程序会选择编译为 async 或非 async,这使得它们主要作为 ?async
API 的消费者。
一个 const 示例
关键字泛型倡议组的一个主要驱动因素是我们希望让 Rust 中不同的修饰符关键字彼此保持一致。const WG 和 async WG 都同时考虑引入关键字 trait,我们认为我们可能应该开始相互交流,以确保我们要引入的内容感觉像是同一语言的一部分——并且将来可以扩展以支持更多关键字。
考虑到这一点,对于可能 const
的 trait 界定和声明,我们建议使用 ?const
符号可能不足为奇。const fn
的一个常见混淆来源是它实际上并不能保证编译时执行;它只意味着在 const
编译时上下文中 可能 进行求值。因此,从某种意义上说,const fn
一直是一种声明“可能 const”函数的方式,而没有办法声明“总是 const”函数。稍后将在本文中详细介绍。
沿用前面使用的 Read
示例,我们可以想象一个“可能 const”版本的 Read
trait 会非常相似
trait ?const Read
然后我们可以将其用作 const read_to_string
函数中的界定,像这样
const
就像 ?async
trait 一样,?const
trait 在用作界定时也需要标记为 ?const
。这在 trait 层面进行展示很重要,因为允许将非 const 界定传递给可能 const 的函数,只要在函数体中不调用 trait 方法。这意味着我们需要区分“从不 const”和“可能 const”。
您可能注意到了 trait 声明上的 ?const
以及 trait 方法上的额外 ?const
。这是有意的:它为将来可能添加对 trait 上的“总是 const”或“从不 const”方法的支持敞开了大门。在 ?async
中,我们知道即使整个 trait 是 ?async
,某些方法(例如 Iterator::size_hint
)将永远不会是异步的。这将使得 ?const
和 ?async
trait 使用相同的规则表现相似。
结合 const 和 async
我们已经讨论了 ?async
,也讨论了 ?const
。现在,如果我们将它们一起使用会发生什么?让我们看看如果我们使用我们对 ?const
和 ?async
的设计来扩展 Read
trait,它会是什么样子
trait ?const ?async Read
/// Read from a reader into a string.
const ?async
这看起来确实开始感觉有很多关键字了,对吧?我们准确地描述了正在发生的事情,但存在大量重复。我们知道,如果我们处理的是一个 const ?async fn
,大多数参数可能都需要是 ?const ?async
。但是根据我们目前提出的语法规则,你最终会在所有地方重复这一点。一旦我们开始添加更多关键字,情况可能会变得更糟。不理想!
因此,我们非常渴望确保找到解决办法。我们一直在思考一种摆脱这种情况的方法,我们称之为 effect/.do
符号。这将允许你通过将函数标记为 effect fn
来表示它是“在所有修饰符关键字上泛化的”,并且通过在函数调用后加上 .do
,编译器将能够在函数体中插入所有正确的 .await
、?
和 yield
关键字。
只是为了设定一些期望:这是我们提案中最不成熟的部分,我们不打算在完成其他一些提案之前正式提出这个方案。但我们认为它是整个愿景的重要组成部分,所以我们想确保在这里分享它。说了这么多,这是上面我们看到的同一个例子,但这次使用了 effect/.do
符号
trait ?effect Read
/// Read from a reader into a string.
?effect
作为 effect/.do
的一部分,我们希望弄清楚的一件事是,如何实现条件性效果界定(conditional effect-bounds)的编写。例如:可能有一个函数总是异步的,从不 panic,并且在剩余效果上是泛型的。或者就像我们看到的一些 API,例如 Vec::reserve
和 Vec::try_reserve
:panic 或返回错误的能力。这需要更多时间和研究才能弄清楚,但我们相信这是一个可以解决的问题。
添加对类型的支持
我们非常热衷于做的一件事是,不仅仅是添加对 ?async
的支持并将其应用于函数、trait 和 trait 界定。我们也希望 ?async
可以用于类型。这将使生态系统不再需要为 crate 提供同步和异步两个版本。它还将使标准库像我们处理 const 一样逐步“异步化”。
async 类型面临的挑战,尤其是在标准库中,是它们在 async 和非 async 上下文中使用时行为常常必须不同。在最底层,async 系统调用与非 async 系统调用工作方式有些不同。但我们认为我们对此也可能有一个解决方案,即 is_async
编译器内置方法。
假设我们想用一个单独的 ?async open
方法来实现 ?async File
。我们期望它看起来会是这样
/// A file which may or may not be async
?
这将使开发者能够根据是否编译为 async 来使用不同的字段,同时仍然共享一个公共核心。并且在函数体内部,也可以根据上下文提供不同的行为。函数体符号将作为当前不稳定的 const_eval_select
intrinsic 的泛化,并且至少对于函数体,我们预计也将提供类似的 is_const()
编译器内置函数。
一致的语法
正如我们之前在文章中暗示的:我们在语言设计中看到的最大挑战之一是以一种使它们感觉与语言其余部分和谐一致的方式添加功能,而不是使其显得明显不同。而且由于我们正在触及 Rust 的核心内容——我们处理关键字的方式——我们必须在这里格外注意,以确保 Rust 仍然感觉像一门统一的语言。
幸运的是,Rust 能够通过 edition 系统对语言进行表层更改。这不能让我们做很多事情,但它确实允许我们要求语法更改。我们正在探索的一种可能性是利用 edition 系统对 const
和 async
进行一些微小更改,以便它们彼此之间以及与 ?const
和 ?async
感觉更一致。
对于 const
,这意味着在 const
声明和 const
使用之间应该有语法上的区别。就像我们之前在文章中提到的,当你写 const fn
时,你得到的是一个可以在运行时和编译时都进行求值的函数。但是当你写 const FOO: () = ..;
时,那里的 const
的含义保证了编译时求值。一个关键字,不同的含义。因此,出于这个原因,我们正在考虑将 const fn
改为 ?const fn
是否会更合理。这将清楚表明这是一个 可能 进行 const 求值,但不一定非要如此的函数——并且也可以从非 const
上下文调用。
//! Define a function which may be evaluated both at runtime and during
//! compilation.
// Current
const
// Proposed
?const
对于 async
,我们正在考虑一些类似的表层更改。Async WG 正在将“trait 中的 async 函数”设计扩展为一个完全涵盖“async trait”的设计,这主要是出于希望能够为匿名 future 添加 + Send
界定的愿望。Eric Holk 在 《轻量、可预测的 async Send 界定》 中提供了更多详细信息。但这大致会变成以下符号
async
这将使 impl ?async Read
和 impl async Read
相互一致。并且它将为将 trait ?async
trait 传递给 impl async Read
并保证始终解释为 trait async
打开大门。这是另一个很好的提升一致性的地方。
我们正在研究的最后一件事是类型的 async
符号。要在类型上实现固有 ?async
方法,我们当前的设计要求类型也标记为 ?async
。为了使 ?async
和 async
更紧密地结合,我们正在探讨是否也应该要求类型标记为 async
//! Proposed: define a method on a maybe-async type
?
//! Current: define a method on an always-async type
//! Proposed: define a method on an always-async type
我们已经通过 const-generics 系统为“总是 const”的参数实现了类似的功能。它们看起来像这样
函数的每个“总是 const”参数都必须始终用 const
标记,因此要求每个“总是 async”的类型都始终使用 async
标记并非完全没有先例。所以我们正在探索这里可能实现的一些功能。
暂定计划
我们计划最初只专注于 async
和 const
关键字。我们感觉已经准备好开始将我们的一些设计转化为 RFC,并开始提交它们进行审查。在未来几个月,我们预计将开始撰写以下提案(排名不分先后)
- 不带 trait 界定的
?async fn
符号,包括一个is_async
机制。 trait async
声明和界定。trait ?async
声明和界定,trait ?const
声明和界定。- 不带 trait 界定的
?const fn
符号。 struct async
符号和struct ?const
符号。
我们将与语言团队、Const WG 和 Async WG 就这些提案密切合作,在某些情况下(例如 trait async
),我们甚至可能担任顾问角色,由某个 WG 直接主导 RFC。像往常一样,这些提案将经历 RFC-nightly-stabilization 周期。只有当我们完全确定它们没有问题时,它们才会登陆稳定版 Rust。
我们有意尚未将 effect/.do
符号包含在此路线图中。我们预计只有在 nightly 版本上有了 ?async
之后才能开始这项工作,而目前还没有。因此,现在我们将继续在倡议组内部进行设计工作,并暂时不制定引入它的计划。
结论
这就是关键字泛型倡议组的 9 个月进展报告。我们希望能够在 RFC 中提供有关 desugarings、语义和替代方案等方面的更详细信息。我们对过去几个月取得的进展感到非常兴奋!有一件事我觉得我们还没提到,但分享出来可能不错:我们实际上已经对这篇文章中的大部分工作进行了原型设计;所以我们相当有信心这一切实际上 真的 能行。对此我们感到无比兴奋!