ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Rust 泛型关联类型(GAT)深度解析:从 RFC 1598 看关联类型构造器如何开启高阶多态

Rust 泛型关联类型(GAT)深度解析:从 RFC 1598 看关联类型构造器如何开启高阶多态
  • 文档

【免费下载链接】rfcs

RFCs for changes to Rust

项目地址:https://gitcode.com/gh_mirrors/rf/rfcs
点击查看免费下载

泛型关联类型(Generic Associated Types,简称 GAT)是 Rust 类型系统中一项承前启后的核心特性,它让 trait 可以声明带类型/生命周期参数的关联项,从而表达"返回类型生命周期与调用方借用绑定"这类标准Iterator无法描述的模式。本文以 RFC 1598(text/1598-generic_associated_types.md) 为骨架,系统讲解其动机、kind(种)理论背景、关联类型构造器(Associated Type Constructor,ATC)的声明/赋值/约束语法、bounds 求值语义与备选方案,并结合本仓库中后续 RFC 的落地证据(如 async fn 脱糖为 GAT、RPIT 脱糖为 GAT)说明该设计如何演化为今天 Rust 语言的一部分。读完本文,你将完整掌握 GAT 的语法形态、它能解决哪些"高阶多态"问题、它刻意不解决什么,以及它在现代 Rust 中的真实角色。

背景:什么是 kind(种)与高阶多态

要理解 GAT,首先要理解类型系统里的kind(种)概念。kind 常被通俗地称为"类型的类型",这个定义对初学者并不友好,因此 RFC 建议用类型系统本身的类比来理解。

在一个良类型化的语言中,每个表达式都有一个类型。有些类型是语言原生的基础类型(base type),无法用其他类型描述,Rust 中的bool、i64、usize、char都是典型例子;另一些类型则是由其他类型"组合"而成的,函数就是最好的例子:

fn not(x: bool) -> bool { !x }

not的类型是bool -> bool(这里借用了非 Rust 语法的箭头记法),而not(true)的类型是bool。这两者截然不同,正是理解高阶多态(higher-kindedness)的关键。

在 kind 分析中,bool、char、bool -> bool等所有类型都具有 kindtype。与bool是基础类型类似,type是一种基础 kind,而像type -> type这样更复杂的 kind 也存在。Vec就是 kind 为type -> type的项:它接受一个类型参数并求值出一个类型。Vec的 kind 与Vec<i32>的 kind(即type)之间的差别,正类比于not的类型与not(true)的类型之间的差别。注意Vec<T>的 kind 同样是type——即使T是类型参数,Vec仍是在被应用于一个类型,正如not(x)的返回类型仍是bool即使x是变量。

Rust 有一个相对罕见的特性:它有两种基础 kind,而许多处理高阶多态的语言只有type这一种。Rust 的另一种基础 kind 是生命周期参数(lifetime)。如果有一个类型Foo<'a>,那么Foo的 kind 就是lifetime -> type。

高阶项自然也可以接受多个参数:Result的 kind 是type, type -> type;给定vec::Iter<'a, T>,vec::Iter的 kind 是lifetime, type -> type。

高阶 kind 的项常被称为类型算子(type operator),其中求值结果为类型的被称为类型构造器(type constructor)。还存在求值为其他类型算子的类型算子,甚至有一阶类型算子(其 kind 形如(type -> type) -> type,以类型算子为参数)。RFC 1598 刻意不处理如此进阶的情况。

RFC 1598 的目标非常聚焦:允许类型构造器像函数、类型、常量一样关联到 trait 上(即关联类型构造器 ATC)。而其他涉及类型构造器的多态形式——例如为类型构造器而非类型实现 trait——不在本 RFC 范围内。

动机:StreamingIterator用例

RFC 以一个极具代表性的 trait 作为动机示例:

trait StreamingIterator { type Item<'a>; fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>; }

这个 trait 非常有用——它允许一种"按借用发起迭代"的Iterator:next返回的值的生命周期,与传给next的引用的生命周期绑定。一个典型用例是:对向量进行迭代,每次产出重叠的可变子切片(overlapping mutable subslices)。如果使用标准Iterator接口,这种实现是非法的,因为每个切片必须与迭代器存活一样长,而不是与next发起的借用一样长。

这个 trait 在当时的 Rust 中无法表达,正因为它依赖一种高阶多态。RFC 提议把这种特定形式的高阶多态引入 Rust,并命名为关联类型构造器。该特性的核心应用场景正如StreamingIterator所示:定义产出"生命周期与接收者局部借用绑定"的类型的 trait。

详细设计:关联类型构造器

声明与赋值关联类型构造器

RFC 提出了一种非常简单的语法来定义关联类型构造器,它与为类型构造器创建别名的语法非常相似,目标是不给尚未理解高阶多态的用户设置障碍:

trait StreamingIterator { type Item<'a>; }

Item关联项是类型构造器而非类型,这一点通过"它带有一个类型参数"即可清楚判断。

关联类型构造器可以像关联类型一样被加约束(bound):

trait Iterable { type Item<'a>; type Iter<'a>: Iterator<Item = Self::Item<'a>>; fn iter<'a>(&'a self) -> Self::Iter<'a>; }

这里的约束应用于类型构造器的输出,参数被当作高阶参数处理。也就是说,上面的约束大致等价于给 trait 追加如下约束:

for<'a> Self::Iter<'a>: Iterator<Item = Self::Item<'a>>

在 impl 中赋值关联类型构造器,语法与赋值关联类型非常相似:

impl<T> StreamingIterator for StreamIterMut<T> { type Item<'a> = &'a mut [T]; ... }

用关联类型构造器构造类型

一旦 trait 拥有关联类型构造器,就可以将其应用于作用域内任何参数或具体项,既可以在 trait 内部,也可以在 trait 外部,使用的语法与使用关联类型类似。示例如下:

trait StreamingIterator { type Item<'a>; // 在 trait 内部把生命周期参数 'a 应用到 Self::Item 上 fn next<'a>(&'a self) -> Option<Self::Item<'a>>; } struct Foo<T: StreamingIterator> { // 在 trait 外部把具体生命周期应用到构造器上 bar: <T as StreamingIterator>::Item<'static>, }

关联类型构造器还可以用于构造其他类型构造器:

trait Foo { type Bar<'a, 'b>; } trait Baz { type Quux<'a>; } impl<T> Baz for T where T: Foo { type Quux<'a> = <T as Foo>::Bar<'a, 'static>; }

最后,生命周期也可以在关联类型构造器中像其他类型构造器一样省略(elide)。考虑生命周期省略规则,StreamingIterator的完整定义是:

trait StreamingIterator { type Item<'a>; fn next(&mut self) -> Option<Self::Item>; }

在约束中使用关联类型构造器

用户可以用 HRTB(高阶 trait 约束,Higher-Ranked Trait Bounds)来约束参数:要求该参数满足"trait 的关联类型构造器构造出的类型"的某种性质。类型相等约束与 trait 约束两种形式都合法:

fn foo<T: for<'a> StreamingIterator<Item<'a>=&'a [i32]>>(iter: T) { ... } fn foo<T>(iter: T) where T: StreamingIterator, for<'a> T::Item<'a>: Display { ... }

RFC不提议允许对类型构造器本身施加任何形式的约束(无论相等约束还是 trait 约束;trait 约束当然也不可能)。

类型参数的关联类型构造器与PointerFamily模式

上述所有例子都聚焦于生命周期参数的 ATC,但 RFC 同样提议增加类型参数的 ATC:

trait Foo { type Bar<T>; }

RFC不提议把 HRTB 扩展为接受类型参数,这使得类型参数 ATC 的表达力受到限制;这种扩展是值得期待的,但超出本 RFC 范围。

类型参数 ATC 可以借助"family" 模式(family trait)编码其他形式的高阶多态。例如,用PointerFamilytrait 抽象Arc与Rc:

trait PointerFamily { type Pointer<T>: Deref<Target = T>; fn new<T>(value: T) -> Self::Pointer<T>; } struct ArcFamily; impl PointerFamily for ArcFamily { type Pointer<T> = Arc<T>; fn new<T>(value: T) -> Self::Pointer<T> { Arc::new(value) } } struct RcFamily; impl PointerFamily for RcFamily { type Pointer<T> = Rc<T>; fn new<T>(value: T) -> Self::Pointer<T> { Rc::new(value) } } struct Foo<P: PointerFamily> { bar: P::Pointer<String>, }

这一"family 模式"在本仓库的后续 RFC 中被进一步引用与扩展:text/2300-self-in-typedefs.md 在讨论"typedefs 中的Self"时,展示了一个结合PointerFamily与 GAT 的Tree类型写法,用type S = P::Pointer<Tree<T>>这种别名把重复的P::Pointer<Tree<T>>抽取出来,避免了在枚举变体中反复书写:

enum Tree<T, P: PointerFamily> { type S = P::Pointer<Tree<T>>; Nil, Node(T, S, S), }

该 RFC 评论称,这种基于 GAT 的做法"相比本 RFC 所提方案更灵活,尤其在 GAT 场景下",代价是需要引入和教授更多概念。

bounds 与 where 子句的求值语义

关联类型构造器上的约束:视为高阶约束

关联类型构造器上的约束被当作施加在 trait 自身上的高阶约束,这与普通关联类型上的约束行为保持一致。例如:

trait Foo { type Assoc<'a>: Trait<'a>; }

等价于:

trait Foo where for<'a> Self::Assoc<'a>: Trait<'a> { type Assoc<'a>; }
关联类型上的 where 子句:每次使用都需证明

与此相反,关联类型上的 where 子句引入的是"每次使用该关联类型时都必须被证明"的约束。例如:

trait Foo { type Assoc where Self: Sized; }

每次调用<T as Foo>::Assoc都需要证明T: Sized——这与"由 impl 来证明约束"的其他情形恰恰相反。

RFC 作者之一(@nikomatsakis)认为,在某些情况下,将 where 子句放到关联类型构造器上,是处理生命周期 well-formedness 所必需的。确切细节在 RFC 中被有意留白,因为它们会在实现过程中逐渐浮现——这正是 text/3245-refined-impls.md 后来所指出的问题的源头:GAT 在 trait 定义处往往需要额外约束(对应 Rust 仓库 issue #87479),而"精细化 impl(refined impls)"特性允许不需要这些约束的实现把它们省略掉,从而放宽类型接口上的要求。

为什么先实现这一个特性,而不是全套高阶多态

GAT 并不是完整的高阶多态,不支持 Haskell 中流行的那些抽象形式;但它提供了 Rust 特有的大多数高阶多态用例,例如流式迭代器与集合 trait。它很可能也是大多数用户最容易上手的特性——即使不理解高阶多态,也能凭直觉理解 GAT。

该特性有几个棘手的实现挑战,但它规避了其他高阶多态所必需的全部能力:

  • 定义高阶 trait
  • 为类型算子实现高阶 trait
  • 更高阶的类型算子
  • 受高阶 trait 约束的类型算子参数
  • 将类型算子参数应用于某个类型或类型参数

换言之,RFC 选择了一个"增量"路径:先解决最高频的用例,同时为未来更复杂的高阶多态保持前向兼容。

提议语法的优势

提议语法的优势在于它复用了已存在的语法。Rust 早已可以用相同语法为类型构造器创建别名;尽管类型别名在类型解析中不扮演多态角色,但在用户看来它们与关联类型非常相似。设计目标是:许多用户即使完全不知道"高阶多态"这一类型系统概念,也能直接使用带关联类型构造器的类型。

如何教学:统一命名为"泛型关联类型(GAT)"

RFC 指出,"关联类型构造器"这一术语虽然此后成为 Rust 社区的通行说法,但它对用户并不友好——"类型构造器"是类型理论中的晦涩行话。因此 RFC 要求:一旦本 RFC 被接受,应尽快把这一概念简称为"泛型关联类型(generic associated types,GAT)"——因为此前关联类型不能是泛型的,本 RFC 之后就可以。教学上应把 GAT 作为关联类型的高级用例来教授,而不是当作一个独立新特性;"family trait" 之类的模式也应通过书籍或博客等补充文档形式教学。

RFC 同时预见到:GAT 会增加用户使用 HRTB 的频率,因此需要在 HRTB 的教学上投入更多精力。

这一点在后续被完全验证:本仓库的多篇 RFC 都以 GAT 为既有基础设施来展开设计。例如 text/3185-static-async-fn-in-trait.md 明确写道,trait 中的 async fn 会脱糖(desugar)为返回 GAT 的关联函数:

trait Example { async fn method<P0..Pn>(&self) where WC0..WCn; } // 脱糖为: trait Example { type $<'me, P0..Pn>: Future<Output = ()> where WC0..WCn, // 显式 where 子句 Self: 'me; // 由 &self 参数推导出的隐含约束 fn method<P0..Pn>(&self) -> Self::$<'_, P0..Pn> where WC0..WCn; }

类似地,text/3654-return-type-notation.md 指出,trait 中返回位置impl Trait(RPIT,按 text/3425-return-position-impl-trait-in-traits.md 定义)会被编译器脱糖为 GAT:

trait Widgets { // 脱糖后 type $Widgets<'a>: Iterator<Item = u32>; fn widgets(&self) -> Self::$Widgets<'_>; }

这些脱糖不对用户暴露($开头的关联类型不可直接命名),但被用于定义 Return Type Notation(RTN)等后续特性的语义。由此可见,GAT 已从 RFC 文本演化为编译器内部的真实基础设施。此外,text/3617-precise-capturing.md 在讨论impl use<..> Trait语法时,也用 GAT 的"泛型参数 vs 泛型实参"类比来解释精确捕获(precise capturing)的两种直觉;text/2421-unreservations-2018.md 则预期 GAT 落地后人们会在 applicative functor 与 monad 库中使用pure关键字,从而支持将其解保留;text/2094-nll.md 也提到,关联类型构造器与owning_ref类方案结合,可能是建模"自引用结构/存在性生命周期"的可行路径之一。这些都印证了 RFC 对 GAT"解决 Rust 特有高阶多态用例"的判断。

缺点分析

增加语言复杂度

GAT 会给语言增加一个相当复杂的特性(多态地解析类型构造器),需要多处类型系统扩展,使编译器实现更复杂。

更隐蔽的风险在于:虽然语法设计得容易学习,但它也让用户更可能在"想表达别的意思"时误用它——类似于impl .. for Trait与impl<T> .. for T where T: Trait之间的混淆。例如:

// 用户本意是 trait Foo<'a> { type Bar: 'a; } // 但他们写成了 trait Foo<'a> { type Bar<'a>; }

两种写法语义截然不同:前者表示Bar这个类型本身存活不超过'a(类型: 生命周期约束);后者则声明了一个以'a为参数的关联类型构造器。

不是完整的"高阶类型"

GAT 没有带来人们谈论高阶类型时想要的全部能力,例如它无法实现Monad这类 trait。有人可能倾向于一次性实现全部高阶多态特性。但 GAT 与其他高阶多态形式前向兼容,绝不阻碍它们;恰恰相反,它通过解决部分求值(partial application)等也会影响其他高阶多态的实现细节,为后续铺路。

语法与其他高阶多态形式不一致

虽然提议的语法与关联类型、类型别名的语法非常相似,但其他形式的高阶多态(例如为类型构造器实现 trait)很可能无法沿用同一风格的语法。正因如此,定义关联类型构造器的语法,与未来"为类型构造器实现 trait"的语法大概率会很不一样。不过,其他高阶多态形式的语法取决于它们具体启用的能力,在未知特性面前设计一致的语法是困难的。

备选方案

方案一:不加 ATC,把 HRTB 推得更远

一种备选方案是更大力地推动 HRTB,甚至引入一些省略规则使其更易用。当前,StreamingIterator的近似版本可以这样定义:

trait StreamingIterator<'a> { type Item: 'a; fn next(&'a self) -> Option<Self::Item>; }

随后可以用T: for<'a> StreamingIterator<'a>来约束类型,避免生命周期参数污染StreamingIterator出现的每一处。

但这只是部分阻止了StreamingIterator的"传染性",只能表达 ATC 能表达类型中的一部分,本质上是用 hack 绕开限制,而不是等价的替代方案。

方案二:对 ATC 施加限制

所谓"完整高阶多态"通常指允许类型构造器作为其他类型构造器的输入参数——即更高阶的类型构造器。不加限制时,多参数更高阶类型构造器会给类型推断带来严重问题。例如推断类型时,若已知有一个形如type, type -> Result<(), io::Error>的构造器,很难确定它是(), io::Error -> Result<(), io::Error>还是io::Error, () -> Result<(), io::Error>。因此,拥有一等公民高阶多态的语言(如 Haskell 的柯里化规则)往往会对此类高阶项施加限制。

如果 Rust 引入更高阶类型构造器,就需要对它们接受的类型构造器的 kind 施加类似限制。但 ATC 作为一种别名,天然遮蔽了具体类型构造器的真实结构——若想用 ATC 作为更高阶类型构造器的参数,就必须对所有ATC 施加这些限制。RFC 列出其认为必要且充分的限制清单:

  • ATC 的每个参数都必须被应用(applied)
  • 必须按它们在 ATC 中出现的顺序应用
  • 必须恰好应用一次
  • 必须是构造器最左侧的参数

这些限制相当苛刻;已知的好几个 ATC 应用都会受挫,例如为HashMap定义Iterable(其 item 为(&'a K, &'a V),生命周期被应用了两次)。

因此 RFC 决定不对所有 ATC 施加这些限制。这意味着:如果将来语言加入更高阶类型构造器,它们将无法把抽象 ATC 作为参数;但这可以用满足限制的 newtype 绕开,例如:

struct IterItem<'a, I: Iterable>(I::Item<'a>);

未解决的问题与设计取舍

RFC 在"未解决问题"一节明确承认,关于 where 子句在关联类型构造器上的确切细节,以及部分高阶多态实现细节,将随着编译器实现逐步浮现(见前文 bounds 与 where 子句求值 中对 @nikomatsakis 观点的引用)。这体现了 RFC 过程一贯的务实态度:设计出可实现的增量步骤,把更远期的复杂决策留给未来。

从本仓库的演进证据看,这一决策是成功的:GAT 如今已作为编译器脱糖机制、async trait 支持、RPIT-in-trait、精确捕获语法讨论等众多特性的基石(见 text/3185-static-async-fn-in-trait.md、text/3654-return-type-notation.md、text/3617-precise-capturing.md),同时其"trait 定义处需要额外约束"的痛点也被后续特性(如 text/3245-refined-impls.md)持续关注与缓解。

结语

RFC 1598 用一页页详实的语法示例,把一个曾被冠以"高阶多态"宏大名称的能力,落地为 Rust 用户触手可及的"泛型关联类型":type Item<'a>;这样一行语法,让流式迭代器、集合抽象、指针族抽象成为可能。它不是完整的高阶多态,却覆盖了 Rust 生态中最有价值的用例,并为未来留下了前向兼容的空间。理解 GAT 的动机、语法与求值语义,是深入现代 Rust 类型系统(async trait、RPIT、精确捕获乃至 monad 式库设计)的必经之路。如果你想进一步研究该特性的原始文本、教学建议与备选方案,可直接阅读仓库中的 RFC 1598 全文;若想了解 GAT 在编译器中的实际应用,推荐对照 RFC 3185 的 async fn 脱糖章节 与 RFC 3654 的 RPIT 脱糖章节。

  • 文档

【免费下载链接】rfcs

RFCs for changes to Rust

项目地址:https://gitcode.com/gh_mirrors/rf/rfcs
点击查看免费下载

相关推荐

上一篇:HomeAssistant Roborock集成配置完整指南
下一篇:阅读APP书源完整使用指南:三步打造专属小说阅读体验

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进