Day 020 — カスタムイテレータの実装(自作アダプタ型 Windows2<I> と拡張トレイトパターン)

2026-08-17 🔵 中級者 / Phase 2 実装 カスタムイテレータの実装

📚 背景知識(読んでから問題へ)

Day 017〜019ではCounterAlternatorのような「ソースイテレータ」(データを内部で生成し、外部の値を持たない)を実装してきました。今日は一段階進んで、「アダプタイテレータ」――既存の任意のIterator内部に包み込み(wrap)、それを加工した値を返す型――を自分で実装します。標準ライブラリのMap<I, F>Filter<I, P>はまさにこの形をしています。

// 標準ライブラリの Map の概念的な構造(実際はもう少し複雑)
pub struct Map<I, F> {
    iter: I,  // 内側のイテレータをまるごと所有する
    f: F,
}

impl<B, I: Iterator, F> Iterator for Map<I, F>
where
    F: FnMut(I::Item) -> B,
{
    type Item = B;
    fn next(&mut self) -> Option<B> {
        self.iter.next().map(&mut self.f)
    }
}

ここで重要なのは、Map<I, F>が特定の型に対してではなく、任意のI: Iteratorに対してジェネリックに実装されているという点です。あなたがVec::iter()に対して.map()を呼んでも、独自のCounterに対して.map()を呼んでも、コンパイラは呼ばれた具体的なIごとに単相化されたMap<Vec<T>への参照, F>Map<Counter, F>を生成します。

もう1つ今日学ぶのが「拡張トレイト(extension trait)パターン」です。Rustにはオーファンルール(orphan rule: 自分のクレートで定義していない型に対して、自分のクレートで定義していないトレイトを実装できない)があるため、Vec<T>のような標準ライブラリの型に対して勝手にメソッドを生やすことはできません。しかし「Iteratorを実装しているすべての型に対して、自分で定義したトレイト(に自分で書いたデフォルト実装)を一括で実装する」というブランケット実装(blanket impl)を使えば、標準の型・自作の型を問わず.windows2()のような独自メソッドを追加できます。これはitertoolsクレートが.tuple_windows().chunks()のようなメソッドを任意のイテレータに追加している仕組みそのものです。

📝 問題

std::sliceにあるwindows(2)(スライスに対して重なり合う2要素のウィンドウを順に返す)と同等の機能を、任意のイテレータに対して使えるアダプタとして実装してください。

要求1: Windows2<I> 構造体の実装

内側のイテレータIを保持し、(前の要素, 今の要素)のペアを順に返す構造体Windows2<I>を定義し、Iteratorを実装してください。

  • I::Item: Cloneという制約を型に課してよい(値を複製して保持する設計にする)
  • 内側のイテレータが[a, b, c, d]を生成する場合、Windows2(a,b), (b,c), (c,d)の順に3つのペアを返す
  • 内側の要素が0個または1個しかない場合、Windows2は1つも値を返さない(Noneを即座に返す)

要求2: 拡張トレイト Windows2Ext の実装

Iteratorを実装しているあらゆる型(標準のVec::into_iter()Range・自作のCounter等)に対して.windows2()という新しいメソッドを追加する拡張トレイトWindows2Extを定義し、ブランケット実装(impl<I: Iterator> Windows2Ext for I where I::Item: Clone {}のような形)で、Iteratorを実装するすべての型に自動的に行き渡るようにしてください。

要求3: 動作確認

以下2パターンをテストで確認してください。

  • vec![1, 2, 3, 4].into_iter().windows2()[(1,2), (2,3), (3,4)]を返す
  • 要素が1個以下の場合(例: vec![1].into_iter().windows2())に何も返さない(.next()が最初からNone

🔍 ヒント(段階的開示)

ヒント1 — 方向性
  • Windows2<I>prev(直前の要素)をOption<I::Item>として保持しておく必要があります。最初のnext()呼び出し時点ではprevNoneなので、まず内側から2つ値を取り出してからペアを作り始める、という初期化ロジックが要ります
  • 拡張トレイトは「新しいメソッドを生やしたいトレイト」を自分で定義し、それを対象にしたい型(ここではIteratorを実装するすべての型)に対してブランケット実装する、という2段構えです
ヒント2 — アプローチ
  • next()の実装は次の分岐になります。self.prevNoneなら、内側から1つ目を取り出してself.prevにセットし、続けて2つ目を取り出す。もし2つ目が取れなければ(内側が1要素以下だった)その場でNoneを返して終了。2つ目が取れたら(prev.clone(), 2つ目.clone())を返しつつ、self.prevを2つ目に更新する
  • 2回目以降のnext()では、self.prevが既にSomeなので、内側から1つだけ取り出し、それがあれば(prev.clone(), 新要素.clone())を返してprevを更新、なければNone
  • Windows2Extは次のような最小限の形で十分です。trait Windows2Ext: Iterator + Sized { fn windows2(self) -> Windows2<Self> where Self::Item: Clone { Windows2 { inner: self, prev: None } } }。そしてimpl<I: Iterator> Windows2Ext for I {}で全イテレータに一括付与します(メソッド自体にwhere制約を付ければ、トレイト自体は無条件で全イテレータに実装できます)
ヒント3 — コード骨格
struct Windows2<I: Iterator> {
    inner: I,
    prev: Option<I::Item>,
}

impl<I: Iterator> Iterator for Windows2<I>
where
    I::Item: Clone,
{
    type Item = (I::Item, I::Item);

    fn next(&mut self) -> Option<Self::Item> {
        if self.prev.is_none() {
            self.prev = self.inner.next();
        }
        let prev = self.prev.clone()?;
        let current = self.inner.next()?;
        self.prev = Some(current.clone());
        Some((prev, current))
    }
}

trait Windows2Ext: Iterator + Sized {
    fn windows2(self) -> Windows2<Self>
    where
        Self::Item: Clone,
    {
        Windows2 { inner: self, prev: None }
    }
}

impl<I: Iterator> Windows2Ext for I {}

模範解答

struct Windows2<I: Iterator> {
    inner: I,
    prev: Option<I::Item>,
}

impl<I: Iterator> Iterator for Windows2<I>
where
    I::Item: Clone,
{
    type Item = (I::Item, I::Item);

    fn next(&mut self) -> Option<Self::Item> {
        // prev がまだ埋まっていなければ、内側から1つ取り出して初期化する
        if self.prev.is_none() {
            self.prev = self.inner.next();
        }
        // prev が取れなかった(内側が最初から空だった)場合はそこで終了
        let prev = self.prev.clone()?;
        // 内側から次の要素を取り出す。取れなければ(もう1要素しか残っていなかった)終了
        let current = self.inner.next()?;
        // 次回の呼び出しのために prev を current で更新しておく
        self.prev = Some(current.clone());
        Some((prev, current))
    }
}

// ---- 拡張トレイト: 任意の Iterator に .windows2() を生やす ----

trait Windows2Ext: Iterator + Sized {
    fn windows2(self) -> Windows2<Self>
    where
        Self::Item: Clone,
    {
        Windows2 {
            inner: self,
            prev: None,
        }
    }
}

// ブランケット実装: Iterator を実装するすべての型に一括付与される
impl<I: Iterator> Windows2Ext for I {}

fn main() {
    // 標準の Vec イテレータに対して独自メソッド .windows2() が使える
    let pairs: Vec<(i32, i32)> = vec![1, 2, 3, 4].into_iter().windows2().collect();
    println!("{pairs:?}");

    // Range に対しても同じメソッドが(ブランケット実装のおかげで)使える
    let range_pairs: Vec<(i32, i32)> = (10..14).windows2().collect();
    println!("{range_pairs:?}");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn windows2_yields_overlapping_pairs() {
        let pairs: Vec<(i32, i32)> = vec![1, 2, 3, 4].into_iter().windows2().collect();
        assert_eq!(pairs, vec![(1, 2), (2, 3), (3, 4)]);
    }

    #[test]
    fn windows2_empty_for_single_or_zero_elements() {
        let one: Vec<(i32, i32)> = vec![1].into_iter().windows2().collect();
        assert_eq!(one, Vec::<(i32, i32)>::new());

        let zero: Vec<(i32, i32)> = Vec::<i32>::new().into_iter().windows2().collect();
        assert_eq!(zero, Vec::<(i32, i32)>::new());
    }

    #[test]
    fn windows2_works_on_range_via_blanket_impl() {
        let pairs: Vec<(i32, i32)> = (10..13).windows2().collect();
        assert_eq!(pairs, vec![(10, 11), (11, 12)]);
    }
}
▶ 実行結果を見る(cargo run / cargo test)
$ cargo run
[(1, 2), (2, 3), (3, 4)]
[(10, 11), (11, 12), (12, 13)]

$ cargo test
running 3 tests
test tests::windows2_empty_for_single_or_zero_elements ... ok
test tests::windows2_works_on_range_via_blanket_impl ... ok
test tests::windows2_yields_overlapping_pairs ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

🪜 Step-by-Step 解説

1
Windows2<I>の状態設計 — なぜOption<I::Item>が必要か
windows(2)が返すペアは「1つ前の要素」と「今の要素」の組です。イテレータは一度next()で取り出した値をもう一度取り出すことができない(Iteratorは前にも後ろにも自由に動けるカーソルではなく、一方向にしか進めない消費型のプロトコル)ため、「1つ前の要素」を自分の構造体の中に保持しておく必要があります。これがprev: Option<I::Item>です。Optionで包んでいる理由は、「まだ1要素も見ていない初期状態」と「すでに1要素以上見た状態」を型で区別するためです。もしprev: I::ItemのようにOptionなしで持とうとすると、構造体を作った瞬間に値が必要になってしまい、「内側が空だった場合」を表現できません。
2
next()の初回呼び出しと2回目以降の非対称性
初回呼び出し時はprevNoneなので、内側から2つ要素を取り出す必要があります(1つ目をprevにセットしてから2つ目を取り出す)。2回目以降の呼び出しでは、prevはすでに前回のペアの2番目の要素で更新済みなので、内側から1つ取り出すだけでペアが作れます。この「初回だけ2つ、以降は1つ」という非対称性は、多くのウィンドウ系アダプタ(itertools::tuple_windows等)に共通する実装パターンです。self.prev.is_none()の分岐1つで、初回とそれ以降のロジックを1つのnext()関数にまとめられている点に注目してください。
3
Windows2Extのブランケット実装とSized境界
trait Windows2Ext: Iterator + Sizedという宣言には2つの制約があります。Iteratorは「このトレイトを実装する型は同時にIteratorでもある」という要求、Sizedは「selfを値として(参照ではなく)受け取るメソッド(fn windows2(self))を定義するために必要」という要求です(dyn Iteratorのようなサイズ不定の型ではselfを値で受け取るメソッドは呼べません)。impl<I: Iterator> Windows2Ext for I {}という1行が、Iteratorを実装するあらゆる具体型(std::vec::IntoIter<T>std::ops::Range<i32>・自作のCounter等)に対して、Windows2Extのデフォルト実装(windows2メソッド)をコンパイル時に行き渡らせます。呼び出し側はuseでこのトレイトをスコープに入れるだけで、まるで標準ライブラリのメソッドであるかのように.windows2()を呼べるようになります。

💡 設計思想・なぜこう書くのか

📌
オーファンルールが「拡張トレイト」という設計パターンを生んだ: Rustは「自分のクレートで定義していない型に、自分のクレートで定義していないトレイトを実装できない」というオーファンルールを持っています。これは「同じ型に対して2つの異なるクレートが競合する実装を与えてしまう」というダイヤモンド問題を防ぐための制約です。しかし同時に、「既存の型に新しいメソッドを追加したい」という自然な欲求も残ります。拡張トレイト(自分でトレイトを定義し、対象の型に対してブランケット実装する)は、この制約と欲求の折り合いをつけるためにRustコミュニティが発展させたイディオムであり、itertoolsrayon.par_iter())・futures.boxed())など主要なエコシステムクレートの大半がこのパターンでAPIを提供しています。
⚠️
「値をClone可能にする」設計判断のトレードオフ: 今回I::Item: Cloneという制約を課したことで実装が大きく簡単になりましたが、これは「大きなデータ構造(巨大なStringVec)を要素に持つイテレータでは、ペアを作るたびに複製コストが発生する」というトレードオフを内包しています。より高性能な実装(std::slice::windowsが実際に採用している方式)は、値を複製せず参照&[T]のスライス)を返すことでコピーコストをゼロにしていますが、その代償として「内側のイテレータが値を所有権ごと消費してしまう任意のIterator」には適用できず、「連続領域を持つスライス」という限定された入力にしか使えません。「汎用性を取るか、ゼロコピーを取るか」はRustのAPI設計で頻出するトレードオフです。

🌐 他言語との比較

観点RustJavaGoJavaScript/TypeScriptPython
既存の型に新メソッドを生やす方法拡張トレイト+ブランケット実装(オーファンルールの制約下でのイディオム)静的メソッド(Utils.windows2(list))が基本。C#の拡張メソッドに相当する言語機能自体はないメソッドを生やす言語機能はなく、素直に関数(Windows2(iter))を書くのが慣習プロトタイプに直接メソッドを追加できる(Array.prototype.windows2 = ...)が、グローバル汚染としてアンチパターン視されるモンキーパッチで既存クラスに後から生やせるが、暗黙的な挙動変化を招くため実務では非推奨
ジェネリックなアダプタの表現力impl<I: Iterator> Iterator for Windows2<I>のように、任意の型パラメータに対して静的に検証された実装を書けるジェネリクスは型消去(type erasure)ベースで、実行時には型情報が失われるジェネリクス(Go 1.18+)はあるが、標準ライブラリのイテレータ抽象化自体が薄く、この種の合成はあまり一般的でない型のないダックタイピングであり、コンパイル時の検証はない(TypeScriptなら型検証は付くが実行時保証はない)ダックタイピング。Iterableプロトコルに従っていれば動くが、静的な保証はない
複製コストの明示性I::Item: Cloneという型制約として明示され、呼び出し側もコンパイルエラーで気づけるCloneableインターフェースはあるが、コレクション操作の多くは参照渡しで暗黙にエイリアシングされる値渡し・参照渡しは型ごとに固定されており、明示的な複製コスト制御はプログラマの手動管理に依存オブジェクトは基本参照渡しなので、複製が必要な箇所はstructuredClone等で都度明示する必要があるオブジェクトは参照渡しが基本。複製が必要な箇所はcopy.deepcopy等で都度明示

Rustの拡張トレイトパターンは、C#の拡張メソッド(public static class Extensions { public static IEnumerable<T> Windows2(this IEnumerable<T> src) ... })に発想としては最も近いものです。ただしC#は言語機能として「拡張メソッド」を直接サポートしているのに対し、Rustは「トレイト+ブランケット実装」という既存の言語機能の組み合わせだけでこれを実現している点が異なります。専用構文を足さずに既存の型システムの表現力だけで同等のことができるのは、Rustのトレイトシステムの汎用性の高さを示す例の1つです。

🏆 実務での使いどころ

  • 時系列データの差分計算: センサーログや株価データのような時系列Vec<f64>に対して.windows2().map(|(prev, cur)| cur - prev)とすれば、隣接データ間の差分(変化量)を1行で計算できます。移動平均・変化率検知・異常値検出の前処理として頻出するパターンです
  • ログ処理でのイベント間隔計算: タイムスタンプ付きログを.windows2()で走査し、(prev.timestamp, cur.timestamp)の差分からイベント間隔の分布を計測する、というのは監視・オブザーバビリティ基盤の実装で日常的に使われる手法です
  • 拡張トレイトパターン自体の実務価値: 社内共通ライブラリで「標準のIteratorに業務固有の集計メソッドを生やしたい」という場面は頻出します(例: .group_consecutive_by()のような独自集計)。今回学んだブランケット実装のパターンは、そのまま社内クレートのAPI設計に転用できます

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
拡張トレイトはimpl Windows2Ext for Vec<T> {}のように対象型を1つずつ書けばよいと思い込む通常のtrait実装に慣れていると、1つの具体型に対して実装するのが自然に見える「あらゆるIterator実装型」に一括で行き渡らせたいならimpl<I: Iterator> Windows2Ext for I {}というブランケット実装が必要。型を1つずつ書くと、対象を増やすたびに実装を追加する羽目になり拡張トレイトの利点が失われる
windows2()を呼ぶだけで自動的に使えると思い込み、use文でトレイトをインポートし忘れる構造体のメソッドはuseなしで呼べることに慣れていると、トレイトメソッドも同様だと錯覚するトレイトのメソッドはそのトレイトが呼び出し側のスコープにuseされていて初めて呼び出せる(コヒーレンスとは別の「メソッド解決のスコープ」規則)。Windows2Extを別モジュールに切り出した場合はuse crate::windows2::Windows2Ext;が必須になる
I::Item: Cloneを課せば必ず値がヒープコピーされ低速になると思い込むCloneという名前から「重い複製」を連想してしまうi32f64のようなCopy型ではCloneはビット単位のコピーであり、ヒープ確保を伴わずほぼ無コスト。重くなるのはStringVec<T>のようなヒープ確保を伴う型をCloneした場合に限られる

🚀 次のステップ

  • 発展: 今回のWindows2<I>をさらに一般化し、ウィンドウサイズを2固定ではなくNVecDeque<I::Item>をリングバッファとして使う)に拡張したWindowsN<I>を実装してみてください。あるいは、値を複製せず&I::Itemの参照ペアを返す設計(内側のイテレータがIterator<Item = &T>である場合に限定すれば複製コストゼロにできる)にも挑戦してみると、Step 2で触れたトレードオフを体感できます
  • 次回予告: 自己評価で「完全理解」または「おおむね理解」が3回連続で記録された時点で、次のテーマスマートポインタ①(Box<T>)(ヒープ確保・再帰的データ構造)に進みます

🎯 自己評価

自分の回答

気づき・メモ