このページの目次

関連型とGAT

関連型(type Item)は、トレイトの実装ごとに1つのItem型しか持てなくし、ジェネリック引数(各実装で無数のTを持てる)と補完し合います——関連型は一意性を強制し、ジェネリック引数は多様性を許可します。GAT(Generic Associated Types、1.65以降)により、関連型自体がジェネリック引数を持てるようになり、自身データを借用して返すトレイトやジェネリックイテレータなど、以前は表現できなかった抽象化が可能になります。

関連型: 各 impl で1つの具体型を指定

trait Iterator {
    type Item;                       // 関連型: 各実装者が具体型を指定
    fn next(&mut self) -> Option<Self::Item>;
}

impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<u32> { ... }
}

なぜジェネリック引数を使わないのか

もちろん、trait Iterator<T> { fn next(&mut self) -> Option<T>; } と書くこともできます。しかし、これは CounterIterator<i32>Iterator<String>Iterator<f64> を同時に実装できることを意味します——各 T ごとに独立した impl となります。Iterator にとってこれは不合理です——1つの型は1種類の値しか生成すべきではありません。

関連型は「1対1」のセマンティック制約を表現します:⁠各型に対して最大1つの impl。ジェネリック引数は「1対多」を表現します:各型は異なるジェネリック引数に対して複数の impl を持つことができます。

これは単なるセマンティックの好みではありません——型推論に影響します。関連型を使用する場合、Iterator::ItemSelf から一意に決定できます。ジェネリック引数を使用する場合、コンパイラは文脈から T を推論する必要があります。関連型はコンパイラの作業を簡素化し、API をより明確にします。

GAT (Generic Associated Types): 関連型にライフタイムを付与

Rust 1.65 で GAT が導入されました——関連型が独自のライフタイムまたはジェネリック引数を持てるようになりました:

trait LendingIterator {
    type Item<'a> where Self: 'a;    // 関連型にライフタイム引数!
    fn next(&mut self) -> Option<Self::Item<'_>>;
}

GAT はどのような問題を解決するのか?従来の Iterator トレイトは「self を借用した参照を返す」ことを表現できません。参照にはライフタイムが必要であるため、type Item = &[u8] と書くことはできません——関連型にはライフタイムを記述する場所がありませんでした。GAT の type Item<'a> により、関連型がライフタイムを使用できるようになり、Item<'a> = &'a [u8] が可能になります——返される参照は &self のライフタイムと関連付けられます。

これは lending iterator と呼ばれます——イテレータが所有権を移すのではなくデータを「貸し出す」ものです。std の Iterator は lending ではありません(nextOption<Self::Item> を返し、&mut self を借用しません)。そのため、イテレータが返す要素に対してイテレータ内部への参照を保持することはできません。GAT はこの制限を解除します。

GAT は async トレイトの基盤でもあります。async fn によって生成される Future 型は匿名であり、トレイト内で表現できません。GAT を使用すると、type Future<'a>: Future<Output = X> という形式が可能になります——トレイトは Box<dyn Future> のヒープ割り当てオーバーヘッドなしで、Future を返すメソッドを持つことができます。

戻り値の impl Trait

fn make_iter() -> impl Iterator<Item = i32> {
    (0..10).filter(|x| x % 2 == 0)    // 匿名型を返す
}

呼び出し元は返される具体型を知りません——「Iterator<Item = i32> を実装している」ということだけを知っています。コンパイラは知っているため、静的ディスパッチとインライン化を行うことができます。Box<dyn Iterator> との違い:impl Trait は静的ディスパッチ(ゼロオーバーヘッド、仮想関数呼び出しなし)ですが、呼び出し元は異なる戻り型を持つ関数を互いに代入できません——これはコンパイラの抽象化境界のコストです。

参考

  • RFC 1598: Generic Associated Types
  • Rust Blog: "GATs are stable!" (2022)
  • Rust Book: Chapter 19.4

Keywords: associated type, GAT, lending iterator, impl Trait, static dispatch, type inference