作者:CrLF0710 / 后期编辑:张汉东


这名字好长!这是啥子哟?

不要紧,我们从头理理。咱们先来温习一下Rust的语法结构。Rust程序是什么构成的?答案是:条目(item)。

每个Rust程序都是一条一条的,每个都是一个条目。比如你在main.rs里写一个结构体定义, 再写一个实现定义为它加两个方法,再写一个main函数。这就是crate对应的模块条目里的三个条目。

说完了条目,咱们再来说关联条目。关联条目不是条目!重点就是在“关联”俩字上,什么是关联呢? 其实就是“跟某个类型有关”,效果就是可以使用一个专门的关键字叫做Self。 这个关键词就是用来指代刚才说的那个类型的。

关联条目可以定义在两处,一个是特质定义的花括号中,一个是实现定义的花括号中。

关联条目一共有三种:关联常数,关联函数,关联类型(别名);它们与条目中的三种:常数、函数、类型(别名) 一一对应。

举个栗子吧!

  1. #![feature(generic_associated_types)]
  2. #![allow(incomplete_features)]
  3. const A: usize = 42;
  4. fn b<T>() {}
  5. type C<T> = Vec<T>;
  6. trait X {
  7. const D: usize;
  8. fn e<T>();
  9. type F<T>; // 新加的就是这个!之前在这里不可以写<T>
  10. }
  11. struct S;
  12. impl X for S {
  13. const D: usize = 42;
  14. fn e<T>() {}
  15. type F<T> = Vec<T>;
  16. }

这个有啥用咧?

蛮有用的,但是仅仅是在特定的场景之下。Rust社区里对泛型关联类型有两个用例是非常经典的,我们先试着介绍它们一下。

但是在开始介绍之前,我们还要再来温习一下泛型。泛型这个词英文是generic,其实是通用的意思。 泛型类型是什么呢?简单来说,就是缺点什么参数的类型,让使用的人填充。

顺便说一下,前人把它意译取名叫泛型,因为很多系统里能填的参数是类型。其实在Rust里面,不只是类型可以当泛型参数。 泛型参数有三种:类型、生存期、常数。

好,我们来看一个具体的泛型类型的例子:Rc<T>,它是具有一个泛型参数的泛型类型。 泛型类型Rc不是类型哈,只有提供了这个泛型参数的“实参”,才是真正的类型,比如Rc<bool>

如果我写一个数据结构,里面要共享数据,但是我事先不知道使用者到底需要我在这里用Rc还是Arc,怎么办呢? 最简单的方法就是代码写两遍,这个听起来有点笨拙,确实也是如此,但是也是确实有效的。 随口一提,http://crates.io上有俩库im和im-rc代码主要区别就是里面用的是Arc还是Rc。 实际上泛型关联类型就可以很好的解决这个问题,接下来, 我们就来看泛型关联类型的第一个经典使用场景:类型家族(type family)。

任务#1:用泛型关联类型支持类型家族

好,现在我们来做一个“选择器”,让编译器根据这个选择器来知道需要用的到底是Rc<T>还是Arc<T>。代码长这样:

  1. trait PointerFamily {
  2. type PointerType<T>;
  3. }
  4. struct RcPointer;
  5. impl PointerFamily for RcPointer {
  6. type PointerType<T> = Rc<T>;
  7. }
  8. struct ArcPointer;
  9. impl PointerFamily for ArcPointer {
  10. type PointerType<T> = Arc<T>;
  11. }

挺简单的吧,这样你就定义了两个“选择器”类型,可以用它来代表要用的是Rc还是Arc。实际用用看:

  1. struct MyDataStructure<PointerSel: PointerFamily> {
  2. data: PointerSel::PointerType<T>
  3. }

这样你泛型参数用RcPointer或者ArcPointer 就可以选择实际的数据表示了。 有了这个功能,刚才说的两个包就可以合成一个包了。好耶~

任务#2:用泛型关联类型实现流式处理迭代器

另一个问题其实是Rust比较特有的,其他语言里,要么不存在这个问题(古尔丹:代价是什么呢?), 要么,放弃治疗这个问题(咳咳)。

这个问题是这样的,希望在API接口上表示输入值与输入值之间、输入值与输出值之间的依赖关系。 依赖关系并不是一个很容易表达出来的东西。Rust的方案是什么呢? 在Rust里,这个人见人爱的生存期小标记'_大家都见过啦。它就负责在API上表示这种依赖关系的对应。

我们来实际用用这个生存期标记,标准库里的迭代器特质大家都见过,它长这样:

  1. pub trait Iterator {
  2. type Item;
  3. pub fn next(&'_ mut self) -> Option<Self::Item>;
  4. // ...
  5. }

挺好的,但是有个小问题。Item类型的值是与Iterator本身的类型(Self)完全不能有依赖关系的。为什么呢? 因为你从Iterator取一个值这个动作,产生的这个临时范围(也就是上面的’_),是next这个关联函数的泛型参数。 定义的Item是单独的另一个关联类型,怎么可能用到呢?

大多数时候这个不是什么问题,但是对于某些库的API来说,这个就不够用了。 比如假如有一个迭代器,依次递给用户一些临时文件用,用户什么时候关闭都可以。这个时候你用Iterator,没有任何问题。 但是要是每次生成一个临时文件,加载一个什么数据,你用完之后它需要关闭临时文件来删除的那种, 这个迭代器肯定就会希望你能够告诉它你用完了。这样它就可以删掉临时文件了, 或者干脆不删除,而是直接复用它的存储空间来存下一个文件,这些都是ok的。

所以这个时候我们可以用泛型关联类型来设计这个API。

  1. pub trait StreamingIterator {
  2. type Item<'a>;
  3. pub fn next(&'_ mut self) -> Option<Self::Item<'_>>;
  4. // ...
  5. }

实现时你其实就可以让Item的类型是一个带依赖的类型,比如一个借用, 类型系统能够保证你在下次调用next或者移动析构这个迭代器之前,Item已经不再被用户使用了。好耶~

你讲的太接地气了,能不能来点抽象的?
好嘞,从现在起我们开始不说人话了。先说一下,这里要说的依然是简化过的,比如我们会把各种binder和predicate放一边。

首先我们来建立泛型类型的名字和具体类型之间的关系。当然就是个映射关系了。

  1. /// 伪代码
  2. fn generic_type_mapping(_: GenericTypeCtor, _: Vec<GenericArg>) -> Type;

比如Vec<bool>中,Vec就是这个泛型类型的名字也是它的构造器, <bool> 是这个泛型参数的列表,就一项。经过了这个映射,得到了一个Vec<bool>

好,然后是特质,啥是特质啊,特质其实也是一个映射。

  1. /// 伪代码
  2. fn trait_mapping(_: Type, _: Trait) -> Option<Vec<AssociateItem>>;

这里这个Trait可以起到一个谓词的作用,也就是拿它来对某个类型做判定,结论要么是None,表示“不符合这个特质”, 要么是一个Some(items) ,表示“这个类型符合这个特质”,并映射出一串关联条目。

  1. /// 伪代码
  2. enum AssociateItem {
  3. AssociateType(Name, Type),
  4. GenericAssociateType(Name, GenericTypeCtor), // 这次新加的
  5. AssociatedFunction(Name, Func),
  6. GenericFunction(Name, GenericFunc),
  7. AssociatedConst(Name, Const),
  8. }

这里的AssociateItem::GenericAssociateType是当前rust里唯一一处间接地执行generic_type_mapping的地方。 通过给trait_mapping的第一个参数传不同的Type,就可以用相同的Trait获取到不同的GenericTypeCtor, 然后执行generic_type_mapping,从而在Rust的语法框架下达到了让不同的GenericTypeCtor跟指定的Vec<GenericArg>组合的目的!

顺便提一下,GenericTypeCtor这类东西,就是某些文章里面介绍的HKT。通过以上描述的这套方法,Rust里第一次加入了供用户使用的HKT能力。 虽然只有这一种形式,但是其他使用形式都可以通过这一种形式做出来。总之就是,奇怪的能力增加了!

我和小鸭子学走路
好嘞,作为收尾,我们来试着用 GAT 仿制一些其他语言的一些构造。

  1. #![feature(generic_associated_types)]
  2. #![allow(incomplete_features)]
  3. trait FunctorFamily {
  4. type Type<T>;
  5. fn fmap<T, U, F>(value: Self::Type<T>, f: F) -> Self::Type<U>
  6. where
  7. F: FnMut(T) -> U;
  8. }
  9. trait ApplicativeFamily: FunctorFamily {
  10. fn pure<T>(inner: T) -> Self::Type<T>;
  11. fn apply<T, U, F>(value: Self::Type<T>, f: Self::Type<F>) -> Self::Type<U>
  12. where
  13. F: FnMut(T) -> U;
  14. }
  15. trait MonadFamily: ApplicativeFamily {
  16. fn bind<T, U, F>(value: Self::Type<T>, f: F) -> Self::Type<U>
  17. where
  18. F: FnMut(T) -> Self::Type<U>;
  19. }

然后我们来给一个“选择器”实现这些类型:

  1. struct OptionType;
  2. impl FunctorFamily for OptionType {
  3. type Type<T> = Option<T>;
  4. fn fmap<T, U, F>(value: Self::Type<T>, f: F) -> Self::Type<U>
  5. where
  6. F: FnMut(T) -> U,
  7. {
  8. value.map(f)
  9. }
  10. }
  11. impl ApplicativeFamily for OptionType {
  12. fn pure<T>(inner: T) -> Self::Type<T> {
  13. Some(inner)
  14. }
  15. fn apply<T, U, F>(value: Self::Type<T>, f: Self::Type<F>) -> Self::Type<U>
  16. where
  17. F: FnMut(T) -> U,
  18. {
  19. value.zip(f).map(|(v, mut f)| f(v))
  20. }
  21. }
  22. impl MonadFamily for OptionType {
  23. fn bind<T, U, F>(value: Self::Type<T>, f: F) -> Self::Type<U>
  24. where
  25. F: FnMut(T) -> Self::Type<U>,
  26. {
  27. value.and_then(f)
  28. }
  29. }

好嘞,然后我们就可以通过OptionType这个“选择器”来表达、使用Option作为Functor, Applicative, Monad 的性质了。 怎么样,是不是打开了无数的新的可能性?