関連型と GAT

このページの目次

トレイトのジェネリック引数は実装の選択に関与し、関連型は選択された実装によって決定されます。例えば Iterator::Item は「このイテレータは何を生成するか」を表し、next を呼び出すたびに任意の出力タイプを再選択させるものではありません。

一意性は完全な実装パラメータに対して相対的である

Iterator には追加の型パラメータがないため、具体的なイテレータ型の Item はその実装によって決定されます。もしトレイト自体にジェネリック引数がある場合、実装はそれらの引数とともに決定する必要があり、「関連型があるからといって、Self ごとに常に 1 つの impl しか存在しない」と一般化することはできません。

trait Convert<Input> {
    type Output;
    fn convert(input: Input) -> Self::Output;
}
struct Decoder;
impl Convert<u8> for Decoder {
    type Output = u16;
    fn convert(input: u8) -> u16 { u16::from(input) }
}
impl Convert<bool> for Decoder {
    type Output = &'static str;
    fn convert(input: bool) -> &'static str { if input { "yes" } else { "no" } }
}
fn main() {
    assert_eq!(<Decoder as Convert<u8>>::convert(7), 7u16);
    assert_eq!(<Decoder as Convert<bool>>::convert(true), "yes");
}

Input は実装を選択するための入力であり、Output はその実装によって指定される結果です。完全修飾表記はこれら2つを明確にします。Iterator<Item = u8> という制約は、選択された実装の関連型が u8 に等しいことを要求します。関連型

等制約と能力制約は異なる問いに答える

I: Iterator<Item = u8> は出力がちょうど u8 であることを要求します。一方、I: Iterator, I::Item: Display は出力がフォーマット可能であることを要求するだけであり、後者のインターフェースは複数の出力タイプを受け付けることができ、呼び出し側はそれらをすべて String に変換する必要はありません。

use std::fmt::Display;
fn joined<I>(input: I) -> String
where
    I: IntoIterator,
    I::Item: Display,
{
    let mut output = String::new();
    for (index, value) in input.into_iter().enumerate() {
        if index != 0 { output.push(','); }
        use std::fmt::Write;
        write!(&mut output, "{value}").unwrap();
    }
    output
}
fn main() {
    assert_eq!(joined([1, 2, 3]), "1,2,3");
    assert_eq!(joined(["a", "b"]), "a,b");
    assert_eq!(joined(std::iter::empty::<u8>()), "");
}

ここで行われているフォーマットは単純な連結であり、CSV シリアライゼーションではありません。入力自体にカンマや改行が含まれている場合、関数はエスケープしません。関連型の制約はコンパイル時に利用可能な操作を記述するものであり、アルゴリズムにデータフォーマットのルールを補完するものではありません。

表記表現される関係
T: Convert<u8>u8 に対する T の実装を選択
<T as Convert<u8>>::Outputその実装によって指定された出力タイプを射影
T: Convert<u8, Output = u16>出力が u16 に等しいことをさらに要求
I::Item: Display出力タイプを固定せず、その能力のみを制約

通常の Iterator は何を借用できるか

std::slice::Iter<'s, T> は Item = &'s T を実装できるため、「Iterator は参照を返せない」というのは誤りです。これらの参照はイテレータが指す外部のスライスから借用されており、ライフタイム 's はすでにイテレータ型の中に含まれています。

困難なのは、返される項目が事前に固定された外部ライフタイムではなく、今回の &mut self から借用するようにすることです。例えば、イテレータ内部で読み取りバッファを再利用する場合、次の読み取りは前回の内容を上書きします。また、重複する可変ウィンドウを順に借用する場合、呼び出し側が同時に2つのウィンドウを保持できないようにする必要があります。

通常の IterMut がなぜ2つの &mut を同時に保持できるか

fn main() {
    let mut values = [1, 2, 3];
    let mut iter = values.iter_mut();
    let first = iter.next().unwrap();
    let second = iter.next().unwrap();
    *first = 10;
    *second = 20;
    assert_eq!(values, [10, 20, 3]);
}

2つの返された参照が同時に使用できるのは、IterMut が毎回異なる要素を渡し、その後のイテレーションで既に借用された同じ位置を再び渡さないことを保証しているからです。その Item は外部スライスのライフタイム上の &mut T であり、今回の next 呼び出しによるイテレータへの短い借用に束縛されたものではありません。IterMut

しかし、重複するウィンドウではこの保証を行うことができません。長さが2の最初のウィンドウはインデックス 0, 1 をカバーし、2番目は 1, 2 をカバーします。これらが同時に書き込み可能であれば、インデックス 1 には2つのアクティブな排他的アクセス入口が存在することになります。したがって、重要な違いはデータが重複しているかどうか、およびインターフェースが返り値の生存期間をどれほど許容するかです。「可変参照を返すなら GAT が必要」というわけではありません。

GAT:借用に応じて変化する関連型の族

next(&'a mut self)排他的借用イテレータItem<'a> = &'a mut [u8]ウィンドウの借用を返すウィンドウがまだ使用されている間は、イテレータを再度借用できないそのウィンドウの使用が終了してから、次のウィンドウを取得できるGAT は今回の借用のライフタイムを返り値の型に持ち込む通常の Iterator の Item は1つの実装に対して固定されているが、GAT は今回の借用に応じて型の族を形成できる。
trait LendingIterator {
    type Item<'a> where Self: 'a;
    fn next(&mut self) -> Option<Self::Item<'_>>;
}

struct Windows<'s> {
    data: &'s mut [u8],
    width: usize,
    position: usize,
}
impl<'s> Windows<'s> {
    fn new(data: &'s mut [u8], width: usize) -> Self {
        assert!(width > 0);
        Self { data, width, position: 0 }
    }
}
impl LendingIterator for Windows<'_> {
    type Item<'a> = &'a mut [u8] where Self: 'a;
    fn next(&mut self) -> Option<Self::Item<'_>> {
        let end = self.position.checked_add(self.width)?;
        if end > self.data.len() { return None; }
        let start = self.position;
        self.position += 1;
        Some(&mut self.data[start..end])
    }
}
fn main() {
    let mut bytes = [1, 2, 3];
    let mut windows = Windows::new(&mut bytes, 2);
    {
        let first = windows.next().unwrap();
        first[1] = 9;
    }
    {
        let second = windows.next().unwrap();
        assert_eq!(second, &[9, 3]);
        second[0] = 8;
    }
    assert!(windows.next().is_none());
    assert_eq!(bytes, [1, 8, 3]);
}

Item<'a> は今回の借用の長さを返り値に持ち込みます。where Self: 'a は、この関連型を使用する際に、Self に関連する借用がそのライフタイム要件を満たすことを意味しており、Self が永続的に生存することを要求するものではありません。

first を取得した後に、first をまだ使用している状態で再度 windows.next() を呼び出すと、コンパイラは重複する排他的借用を拒否します。ウィンドウ同士は基盤となる位置を共有できますが、2つのアクティブな可変借用を通じて同時にアクセスすることはできません。この例では unsafe は必要ありません。

インデックス 0初期値 1インデックス 1初期値 2インデックス 2初期値 3first:[0, 1] の 1 → 9second:[1, 2] で 9 を読む最初の借用最初の使用が終了した後重複ウィンドウは位置を共有するが、借用は時分割で行う必要があるインデックス 1 の変更は基盤の配列に保持されます。借用の終了はデータをロールバックせず、ウィンドウのコピーも不要です。

項目ごとに消費するか、明示的に独立した所有権を取得するか

処理ロジックが統計量の計算のみを必要とする場合、next のたびに所有権を持つ結果を即座に取得し、次の項目を取得することができます。完全なウィンドウを保存したい場合は、Vec にコピーできます。これにより割り当てとコピーのコストが増加しますが、結果が現在のウィンドウの借用に依存していることが明確に解消されます。ウィンドウ参照をそのまま収集すると、重複する可変ウィンドウの安全性の目的と衝突します。

上記の main 関数における first の中括弧は、使用順序を明確に示すためのものでした。借用は通常、その後の使用に基づいて解析されるため、変数の字面スコープの終了を待つ必要はありません。second を先に取得してから first を読み取ると、コンパイラは first がまだアクティブであるとみなすため、2回目の排他的借用を拒否します。

境界入力上記の Windows の動作理由
width = 0構築時にパニック例では空ウィンドウのセマンティクスを明示的に除外している;公開インターフェースでは Result を使用するのが適切
width がデータ長より大きい最初の next で None を返す完全なウィンドウが存在しない
width がデータ長に等しい1つのウィンドウのみを生成次の開始点は width を収容できない
幅が2、データ長が3互いに重複する2つのウィンドウを生成開始点は毎回1ずつ増加する
開始点に幅を加算した結果がオーバーフローnext が None を返すchecked_add が表現できない終端を拒否する

where Self: 'a は何を制約しているか

メソッドシグネチャを展開して理解すると fn next<'a>(&'a mut self) -> Option<Self::Item<'a>> のようになります。Item は固定された型ではなく、ライフタイム引数を受け取る型の族です。今回の呼び出しで選択された 'a が返り値に入り、借用チェッカーが「ウィンドウがまだ使用されていること」と「イテレータがまだ借用されていること」を関連付けることができます。

外部借用を含む Windows<'s> において、Self: 'a はこれらの借用が 'a に対して有効であることを要求します。寿命の短い基盤データからより長いウィンドウを派生させることはできません。GAT 宣言上の where 条件は、実装が依存できる前提を提供するもので、例では self を借用するメソッドの関係のためにこれを記述する必要があります。これは「イテレータオブジェクトがプログラムの終了まで生存しなければならない」という意味でも、実行時の寿命チェックでもありません。GAT と必須の where 節

このトレイトは std::iter::Iterator ではなく、標準イテレータの組み合わせメソッドは自動的にすべて適用されません。GAT は表現可能なインターフェースを拡張しますが、それに対応するアダプタエコシステムを自動的に生成するものではありません。また、現在のコンパイラが複雑な制約や dyn 互換性に対してどの程度サポートしているかの境界も考慮する必要があります。

ジェネリック使用が自動的に dyn をサポートするわけではない

GAT は Item<'a> を記述できますが、このような関連型を持つレンドインターフェース据此直接 dyn LendingIterator になるわけではありません。通常の dyn Iterator<Item = u8> は1つの出力タイプを固定するアプローチであり、ライフタイムに応じて変化する型の族全体を自動的に解決するわけではありません。動的境界が必要な場合は、処理動作をトレイトメソッド内に配置するか、所有権を持つデータを返すことを検討してください。選択はコピーを許容できるかどうか、および呼び出し側がどのような組み合わせ能力を必要とするかによって異なります。

複雑な消費者は for<'a> 高階ライフタイム制約を必要とする場合があります。これは量化されたライフタイムに対して条件が満たされることを意味し、特定の命名されたライフタイムや 'static に機械的に置き換えることはできません。コンパイラの診断に遭遇した場合は、実際のトレイト、where 条件、およびコンパイラのバージョン検証に焦点を当てて範囲を狭めるべきであり、エラーを消去するために借用されたデータに無謀に 'static を付与すべきではありません。

重なるウィンドウを同時に保持できない理由

幅 3 のウィンドウの重なりを見てから、第 2 ウィンドウを重ならない場所に移します。それでも旧出力を使う間は lending インターフェースで self を再借用できません。シグネチャは出力を今回の借用に結び付け、毎回の範囲計算を証明するものではありません。通常の IterMut は実装が出力同士の非重複を保証します。

図を準備しています
重なるウィンドウを同時に保持できない理由

lending の出力は今回の &mut self に結び付き、旧出力を後で使う間は、範囲が偶然重ならなくても次の next はできません。

返り位置の impl Trait:隠された具体型

fn selected(even: bool) -> impl Iterator<Item = i32> {
    (0..6).filter(move |value| (*value % 2 == 0) == even)
}
fn main() {
    assert_eq!(selected(true).collect::<Vec<_>>(), [0, 2, 4]);
    assert_eq!(selected(false).collect::<Vec<_>>(), [1, 3, 5]);
}

2つのブランチの動作はクロージャがキャプチャした bool によって決定されますが、返される隠された型は同じままです。if の両側でそれぞれ Range と Filter を返した場合、それらがともに Iterator を実装しているからといって自動的に統一されません。統一された構造体、明示的な enum、または dyn を使用して差異を消去する必要があります。impl Trait

隠された型は関数のジェネリック引数に依存することができ、ライフタイムキャプチャルールはエディションによっても異なるため、「すべての呼び出しでグローバルに1つの型しかない」というのも過度に単純化されています。インターフェースが約束するトレイト制約は、呼び出し側が依存できる能力であり、基盤となるイテレータの組み合わせ名を公開するものではありません。

異なる実行時の選択を同じ構造体で収容する

前述の selected は選択をクロージャの状態に格納しているため、2回の呼び出しで同じ返り値型を持ちます。もう1つの一般的なケースは「要素がない可能性がある」場合です。ブランチを Option に変換し、同じ IntoIter を返すことができます。

fn maybe_value(enabled: bool) -> impl Iterator<Item = u8> {
    enabled.then_some(7).into_iter()
}
fn main() {
    assert_eq!(maybe_value(true).collect::<Vec<_>>(), [7]);
    assert!(maybe_value(false).collect::<Vec<_>>().is_empty());
}

true ブランチで std::iter::once(7) を返し、false ブランチで std::iter::empty() を返すように変更した場合、両者は同じ Item を持ちますが、異なる具体型であり、直接同じ返り位置の impl Iterator になることはできません。この制限は要素の数とは無関係です。同じ型であっても、実行時に異なる数の要素を生成することは完全に可能です。

非同期トレイトとの関連

GAT は借用を持つ関連 Future 型を表現でき、インターフェースが返り値と &self の関係を明確にできます。しかし、これは安定版 Rust で、各関連型定義で任意に impl Future を記述して匿名の async ブロックに名前を付けられることを意味しません。

ネイティブな async fn in trait および返り位置の impl Trait in trait は Rust 1.75 で安定化 されており、対応する静的呼び出しに使用できます。これらもまた、自動的にトレイトを dyn で使用可能にするものではありません。動的に Future を消去する必要がある場合、一般的なインターフェースは Pin<Box<dyn Future<Output = T> + 'a>> を返し、Send を要求するかどうかを別途決定します。実際の非同期メカニズムについては async/await を、オブジェクトの制限については トレイトオブジェクト を参照してください。