推动 GATs 稳定化
从哪里开始,从哪里开始呢...
首先要说:这是一篇非常令人兴奋的文章。一些读者会感到非常激动;一些人可能不知道 GATs(泛型关联类型)是什么;另一些人可能会难以置信。此功能的 RFC 确实是在 2016 年 4 月开启的(大约一年半后合并)。事实上,此 RFC 甚至早于 const generics(其 MVP 最近已 稳定)。不过,不要被这蒙蔽了:它是一个强大的功能;GitHub 上跟踪问题的反应也许可以让你了解它的受欢迎程度(它是 Rust 存储库中赞同最多的问题):
如果你不熟悉 GATs,它们允许你在关联类型上定义类型、生命周期或 const 泛型。 就像这样
trait Foo {
type Bar<'a>;
}
现在,这可能看起来平淡无奇,但稍后我将详细介绍为什么这真的是一个强大的功能。
但现在:到底发生了什么?嗯,在其 RFC 合并近四年后,generic_associated_types
功能不再是“不完整的”。
蟋蟀鸣叫
等等...就这样吗?好吧,是的!我将在本文稍后详细介绍为什么这是一件大事。但是,长话短说,为了让 GATs 工作,编译器必须进行大量的更改。而且,虽然仍然有一些小的诊断问题,但该功能最终处于我们认为可以不再将其视为“不完整”的状态。
那么,这意味着什么呢?好吧,它真正的含义是,当你在 nightly 版本上使用此功能时,你将不再收到“generic_associated_types
是不完整的”警告。然而,这之所以是件大事的真正原因是:我们希望稳定这个功能。但我们需要你的帮助。我们需要你测试此功能,为发现的任何错误或潜在的诊断改进提交问题。此外,我们很乐意你在 Zulip 上告诉我们 GATs 启用的一些有趣的模式!
在不做出我们 100% 确定能够实现的承诺的情况下,我们非常希望在未来几个月内能够稳定此功能。但是,我们希望确保我们没有遗漏任何显而易见的错误或缺陷。我们希望这是一个平稳的稳定过程。
好的。呼。这是本文的要点和最令人兴奋的消息。但正如我之前所说,我认为我也有理由解释一下这个功能是什么,你可以用它做什么,以及一些背景以及我们是如何走到这一步的。
那么 GATs 是什么?
注意:这只是一个简短的概述。RFC 包含更多详细信息。
GATs(泛型关联类型)最初是在 RFC 1598 中提出的。如前所述,它们允许你在关联类型上定义类型、生命周期或 const 泛型。如果你熟悉具有“高阶类型”的语言,那么你可以将 GATs 称为特性上的类型构造器。也许你了解如何使用 GATs 的最简单方法是直接看一个示例。
这是一个流行的用例:LendingIterator
(以前称为 StreamingIterator
)
trait LendingIterator {
type Item<'a> where Self: 'a;
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
让我们来看一下它的一个实现,一个假设的 <[T]>::windows_mut
,它允许在切片上迭代重叠的可变窗口。 如果你今天尝试使用 Iterator
实现它,就像
struct WindowsMut<'t, T> {
slice: &'t mut [T],
start: usize,
window_size: usize,
}
impl<'t, T> Iterator for WindowsMut<'t, T> {
type Item = &'t mut [T];
fn next<'a>(&'a mut self) -> Option<Self::Item> {
let retval = self.slice[self.start..].get_mut(..self.window_size)?;
self.start += 1;
Some(retval)
}
}
那么你会收到一个错误。
error[E0495]: cannot infer an appropriate lifetime for lifetime parameter in function call due to conflicting requirements
--> src/lib.rs:9:22
|
9 | let retval = self.slice[self.start..].get_mut(..self.window_size)?;
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
note: first, the lifetime cannot outlive the lifetime `'a` as defined on the method body at 8:13...
--> src/lib.rs:8:13
|
8 | fn next<'a>(&'a mut self) -> Option<Self::Item> {
| ^^
note: ...so that reference does not outlive borrowed content
--> src/lib.rs:9:22
|
9 | let retval = self.slice[self.start..].get_mut(..self.window_size)?;
| ^^^^^^^^^^
note: but, the lifetime must be valid for the lifetime `'t` as defined on the impl at 6:6...
--> src/lib.rs:6:6
|
6 | impl<'t, T: 't> Iterator for WindowsMut<'t, T> {
| ^^
简而言之,此错误本质上是在告诉我们,为了能够返回对 self.slice
的引用,它必须与 'a
的生命周期相同,这将需要一个 'a: 't
绑定(我们无法提供)。如果没有这个,我们可以在已经持有对切片的引用的情况下调用 next
,从而创建重叠的可变引用。但是,如果你使用之前的 LendingIterator
特性来实现它,则可以正常编译
impl<'t, T> LendingIterator for WindowsMut<'t, T> {
type Item<'a> where Self: 'a = &'a mut [T];
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>> {
let retval = self.slice[self.start..].get_mut(..self.window_size)?;
self.start += 1;
Some(retval)
}
}
顺便说一句,关于这个特性和 impl,你可能会好奇一件事:Item
上的 where Self: 'a
子句。 简而言之,这允许我们使用 &'a mut [T]
;如果没有这个 where 子句,有人可能会尝试返回 Self::Item<'static>
并扩展切片的生命周期。我们知道这有时是一个令人困惑的地方,并且正在考虑潜在的替代方案,例如始终假设此绑定或根据特性中的使用情况暗示它(请参阅 此问题)。我们绝对很乐意在这里听到你的用例,特别是在假设此绑定会成为障碍时。
作为另一个示例,假设你希望一个结构体对指向特定类型的指针是泛型的。 你可以编写以下代码
trait PointerFamily {
type Pointer<T>: Deref<Target = T>;
fn new<T>(value: T) -> Self::Pointer<T>;
}
struct ArcFamily;
struct RcFamily;
impl PointerFamily for ArcFamily {
type Pointer<T> = Arc<T>;
...
}
impl PointerFamily for RcFamily {
type Pointer<T> = Rc<T>;
...
}
struct MyStruct<P: PointerFamily> {
pointer: P::Pointer<String>,
}
我们不会在这里深入讨论细节,但这个例子很好,因为它不仅突出了 GATs 中类型的使用,还表明你仍然可以使用已经可以在关联类型上使用的特性绑定。
这两个例子只是 GATs 支持的模式的冰山一角。 如果你发现任何看起来特别有趣或巧妙的模式,我们很乐意在 Zulip 上听到它们!
为什么实现这个需要这么长时间?
那么是什么导致我们花了近四年时间才达到现在的地步?嗯,很难用语言来表达现有的特性求解器必须更改和适应的程度;但是,请考虑一下:一段时间以来,人们认为为了支持 GATs,我们必须过渡 rustc 以使用 Chalk,这是一种潜在的未来特性求解器,它使用逻辑谓词来解决特性目标(虽然已经取得了一些进展,但即使现在仍然非常具有实验性)。
作为参考,以下是一些为以某种方式进一步支持 GAT 而进行的各种实现添加和更改
- 在 AST 中解析 GATs (#45904)
- 解析 GATs 中的生命周期 (#46706)
- 支持生命周期的初始特性求解器工作 (#67160)
- 验证投影边界(并进行允许类型和 const GAT 的更改)(#72788)
- 分离投影边界和谓词 (#73905)
- 允许在特性路径中使用 GAT (#79554)
- 部分地用 universe 替换泄漏检查 (#65232)
- 将泄漏检查移至特性求解的后期 (#72493)
- 在投影时用占位符替换 GAT 中的绑定变量 (#86993)
为了进一步强调以上工作:其中许多 PR 都很大,并且背后有大量的设计工作。一路上还有几个较小的 PR。但是,我们做到了。我只想祝贺以某种方式为此付出努力的所有人。你们真棒。
目前有什么限制?
好的,现在到了没人喜欢听的部分:限制。幸运的是,在这种情况下,实际上只有一个 GAT 限制:具有 GAT 的特性不是对象安全的。这意味着你将无法执行以下操作
fn takes_iter(_: &mut dyn for<'a> LendingIterator<Item<'a> = &'a i32>) {}
做出此决定的最大原因是,仍然需要进行一些设计和实现工作才能使其真正可用。虽然这是一个不错的功能,但在将来添加此功能将是一个向后兼容的更改。我们认为,最好先稳定大多数 GAT,然后再回来尝试解决这个问题,而不是为了进一步阻止 GAT。此外,没有对象安全的 GAT 仍然非常强大,因此我们推迟它并没有损失太多。
正如本文前面提到的,仍然有一些剩余的诊断 问题。如果你确实发现了错误,请提交问题!