GAT 稳定化的推动

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

GAT 稳定化的推动

从哪里开始,从哪里开始...

让我们先说:这是一篇非常令人兴奋的文章。一些读者会欣喜若狂;一些人可能不知道 GAT(泛型关联类型)是什么;另一些人可能难以置信。这个功能的 RFC 是在 2016 年 4 月提出的(并在一年半后合并)。事实上,这个 RFC 甚至早于 const 泛型(最近才 稳定化)。但不要被这所迷惑:它是一个强大的功能;GitHub 上的跟踪问题对它的反应也许能让你了解它的受欢迎程度(它是 Rust 仓库中票数 最高 的问题):GATs 反馈

如果你不熟悉 GAT,它们允许你在关联类型上定义类型、生命周期或 const 泛型。就像这样

trait Foo {
    type Bar<'a>;
}

现在,这可能看起来并不令人印象深刻,但我会在后面详细介绍 为什么 这确实是一个强大的功能。

但现在:究竟 发生了 什么?好吧,在它的 RFC 合并近四年后,generic_associated_types 功能不再是“不完整的”。

蟋蟀鸣叫

等等...就这些?是的!我会在这篇博文的后面详细介绍为什么这 是一件大事。但简而言之,为了让 GAT 正常工作,编译器不得不进行大量的更改。虽然还有一些小的诊断问题,但该功能终于处于一个我们认为可以不再将其标记为“不完整”的状态。

那么,这意味着什么呢?好吧,它 真正 意味着当你使用 nightly 版本中的这个功能时,你将不再收到“generic_associated_types is incomplete”的警告。然而,这真正重要的原因是:我们想要稳定这个功能。但我们需要你的帮助。我们需要你测试这个功能,为任何你发现的 bug 或潜在的诊断改进提交问题。此外,我们也希望你能够在 Zulip 上告诉我们 GAT 启用的有趣模式!

虽然我们不能保证我们能做到,但我们非常希望能在未来几个月内稳定这个功能。但是,我们想确保我们没有错过明显的 bug 或缺陷。我们希望这将是一个平稳的稳定化过程。

好的。呼。这是这篇文章的重点,也是最令人兴奋的消息。但正如我之前所说,我认为我也有必要解释这个功能 是什么,你可以用它做什么,以及一些背景信息和我们是如何走到这一步的。

那么 GAT 是什么呢?

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

GAT(泛型关联类型)最初是在 RFC 1598 中提出的。正如之前所说,它们允许你在关联类型上定义类型、生命周期或 const 泛型。如果你熟悉具有“高阶类型”的语言,那么你可以将 GAT 称为 trait 上的类型构造器。也许让你了解如何使用 GAT 的最简单方法是直接看一个例子。

这里有一个流行的用例: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> 并延长切片的生命周期。我们理解这有时会让人困惑,并且正在考虑潜在的替代方案,例如始终假设这个绑定,或者根据在特性中的使用情况来隐式推断它(参见 这个问题)。我们非常希望听到你在这方面的用例,特别是当假设这个绑定会成为阻碍时。

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

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>,
}

我们不会深入探讨细节,但这个例子很好地说明了 GAT 中类型的使用,同时也表明你仍然可以使用你已经在关联类型上使用的特性绑定。

这两个例子只是 GAT 支持的模式的冰山一角。如果你发现任何特别有趣或巧妙的模式,我们非常希望在 Zulip 上听到你的反馈!

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

那么是什么导致我们花了近四年时间才达到现在的状态呢?好吧,很难用语言描述现有的特性求解器需要进行多少改变和适应;但请考虑以下情况:有一段时间,人们认为为了支持 GAT,我们必须将 rustc 转变为使用 Chalk,这是一个潜在的未来特性求解器,它使用逻辑谓词来解决特性目标(虽然已经取得了一些进展,但它现在仍然非常实验性)。

作为参考,以下是一些以某种方式促进了 GAT 支持的各种实现添加和更改

  • 在 AST 中解析 GAT (#45904)
  • 解析 GAT 中的生命周期 (#46706)
  • 支持生命周期的初始特性求解器工作 (#67160)
  • 验证投影绑定(并进行允许类型和 const GAT 的更改)(#72788)
  • 分离投影绑定和谓词 (#73905)
  • 允许 GAT 出现在特性路径中 (#79554)
  • 部分用宇宙替换泄漏检查 (#65232)
  • 将泄漏检查移至特性求解过程的后期 (#72493)
  • 在投影时用占位符替换 GAT 中的绑定变量 (#86993)

为了进一步强调上述工作:许多 PR 规模很大,背后有相当多的设计工作。在此过程中还有许多较小的 PR。但是,我们做到了。我只想祝贺所有为此付出努力的人。你们很棒。

目前有哪些限制?

好的,现在是大家都不喜欢听到的部分:限制。幸运的是,在这种情况下,实际上只有一个 GAT 限制:具有 GAT 的特性不是对象安全的。这意味着你无法执行以下操作

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

做出这个决定的最大原因是,实际上要让它可用,还需要一些设计和实现工作。虽然这是一个不错的功能,但在未来添加它将是一个向后兼容的更改。我们认为,最好先稳定 大部分 GAT,然后再回来尝试解决这个问题,而不是让 GAT 阻塞更长时间。此外,没有对象安全的 GAT 仍然非常强大,所以我们推迟这个问题并没有损失太多。

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