推动 GATs 稳定化

2021 年 8 月 3 日 · Jack Huey 代表 特性工作组

推动 GATs 稳定化

从何说起,从何说起...

首先要说的是:这是一篇非常令人兴奋的文章。一些读者可能会感到无比激动;一些人可能完全不知道 GATs(泛型关联类型)是什么;还有一些人可能会难以置信。这项功能的 RFC 实际上是在 2016 年 4 月提出的(并在大约一年半后合并)。事实上,这个 RFC 甚至比 const 泛型(其 MVP 版本 最近刚刚稳定)还要早。但这并不能欺骗你:这是一项强大的功能;Github 上跟踪问题的反应或许能让你了解它的受欢迎程度(它是 Rust 仓库中 *投票数最高* 的问题):GATs 的反应

如果你不熟悉 GATs,它们允许你在关联类型上定义类型、生命周期或 const 泛型。例如:

trait Foo {
    type Bar<'a>;
}

现在,这可能看起来平平无奇,但我稍后会更详细地解释 为什么 这确实是一项强大的功能。

但就目前而言:究竟 发生 了什么?在 RFC 合并近四年后,generic_associated_types 功能不再标记为“不完整”(incomplete)。

(一片寂静)

等等... 就这样?? 嗯,是的!我将在本文稍后详细解释 为什么 这很重要。长话短说,为了让 GATs 工作,编译器进行了相当多的修改。而且,虽然仍有一些小的诊断问题有待解决,但该功能最终达到了一种我们认为可以不再将其标记为“不完整”的状态。

那么,这意味着什么呢?嗯,它 真正 意味着的是,当你在 Nightly 版本中使用此功能时,你将不再收到“generic_associated_types 功能不完整”的警告。然而,这件事之所以重要,真正的原因是:我们想稳定这项功能。但我们需要你的帮助。我们需要你测试这项功能,对于发现的任何 bug 或潜在的诊断改进,请提交 issue。另外,如果你发现了 GATs 所能实现的有趣模式,我们非常乐意听到,请到 Zulip 告诉我们!

虽然我们不能百分百确定,但我们非常希望能在接下来的几个月内稳定这项功能。但是,我们想确保没有遗漏任何显而易见的错误或缺陷。我们希望这次稳定化过程能够顺利进行。

好的。松一口气。这是本文的重点和最令人兴奋的消息。但正如我之前所说,我认为解释这项功能 是什么、你可以用它做什么,以及一些背景信息和我们是如何走到这一步的,也是合理的。

GATs 是什么?

注意:这只是一个简要概述。RFC 中包含更多细节。

GATs(泛型关联类型)最初是在 RFC 1598 中提出的。如前所述,它们允许你在关联类型上定义类型、生命周期或 const 泛型。如果你熟悉拥有“更高种类类型(higher-kinded types)”的语言,那么你可以将 GATs 称为 特性上的类型构造器 (type constructors on traits)。也许让你了解如何使用 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)
    }
}

顺带一提,关于这个特性和实现,有一点你可能好奇:Item 上的 where Self: 'a 子句。简单地说,这允许我们使用 &'a mut [T];没有这个 where 子句,有人可能会尝试返回 Self::Item<'static> 并延长切片的生命周期。我们理解这有时会引起困惑,并且正在考虑潜在的替代方案,例如始终假定此约束或根据特性内部的使用情况来暗示它(参见 这个 issue)。我们非常希望听到你们在这里的用例,特别是当假定此约束会成为阻碍时。

再举一个例子,想象你想让一个结构体对指向特定类型的指针进行泛型。你可能会写出以下代码:

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 中类型的使用,同时也展示了你仍然可以使用已有的、可在关联类型上使用的特性约束(trait bounds)。

这两个例子只是 GATs 支持的模式的冰山一角。如果你发现了任何看起来特别有趣或巧妙的模式,我们非常乐意到 Zulip 上听取你的分享!

为什么实现它花了这么长时间?

那么,是什么导致我们花费了将近四年的时间才达到现在这个阶段呢?嗯,很难用语言来形容现有的特性解析器(trait solver)需要进行多少改变和适应;但是,请考虑这一点:有一段时间,人们认为为了支持 GATs,我们将不得不将 rustc 切换到使用 Chalk,这是一个潜在的未来特性解析器,它使用逻辑谓词(logical predicates)来解决特性目标(trait goals)(尽管目前已经取得了一些进展,但它现在仍然非常具有实验性)。

供参考,以下是一些为支持 GATs 而进行的各种实现添加和更改:

  • 在 AST 中解析 GATs (#45904)
  • 解析 GATs 中的生命周期 (#46706)
  • 特性解析器支持生命周期的初步工作 (#67160)
  • 验证投影约束 (projection bounds)(并进行了允许类型和 const GATs 的更改)(#72788)
  • 分离投影约束 (projection bounds) 和谓词 (predicates) (#73905)
  • 允许 GATs 出现在特性路径中 (#79554)
  • 部分用宇宙 (universes) 替换泄露检查 (leak check) (#65232)
  • 将泄露检查 (leak check) 移至特性解析后阶段 (#72493)
  • 在投影时用占位符 (placeholders) 替换 GATs 中的绑定变量 (bound vars) (#86993)

为了进一步强调上述工作:许多这些 PR 都很庞大,背后有大量的设计工作。在此过程中还有一些较小的 PR。但是,我们做到了。我只想祝贺所有以各种方式为此付出努力的人。你们太棒了。

目前有哪些限制?

好的,现在到了没人喜欢听的部分:限制。幸运的是,在这种情况下,GATs 只有一个限制:带有 GATs 的特性不是对象安全的(object safe)。这意味着你将无法做这样的事情:

fn takes_iter(_: &mut dyn for<'a> LendingIterator<Item<'a> = &'a i32>) {}

做出这个决定的最大原因是,要真正使这一点可用,还需要一些设计和实现工作。虽然这是一个不错的功能,但将来添加它会是一个向后兼容的更改。我们认为最好是先稳定 大部分 GATs,然后再回来尝试解决这个问题,而不是为了这一点而进一步推迟 GATs 的稳定化。此外,即使没有对象安全,GATs 仍然非常强大,因此推迟这一点我们并没有损失太多。

正如本文前面提到的,还有一些剩余的诊断 issue。不过,如果你确实发现了 bug,请提交 issue!