あっぽログ
← 記事一覧に戻る

RustのIteratorアダプタを使いこなす:chain・zip・flatMapで高度なデータ処理を実現する

はじめに

RustのIteratorトレイトには、データを効率よく処理するための多彩なアダプタが用意されています。以前の記事でもmapfilterを取り上げましたが、今回はさらに一歩進んでchainzipflat_mapを中心に解説します。これらを組み合わせることで、ループを書かずに宣言的で読みやすいデータ処理が実現できます。


chain:複数のイテレータをつなげる

chainは、2つのイテレータを順番に結合して1つのイテレータとして扱います。

fn main() {
    let first = vec![1, 2, 3];
    let second = vec![4, 5, 6];

    let combined: Vec<i32> = first.iter()
        .chain(second.iter())
        .copied()
        .collect();

    println!("{:?}", combined); // [1, 2, 3, 4, 5, 6]
}

chainは型が同じイテレータを結合するため、異なる型の場合はコンパイルエラーになります。複数のログソースをまとめて処理したり、固定リストと動的リストを組み合わせたりする場面で役立ちます。

3つ以上のイテレータをつなげる

chainは連続して呼び出せます。

fn main() {
    let a = vec!["apple"];
    let b = vec!["banana"];
    let c = vec!["cherry"];

    let fruits: Vec<&str> = a.iter()
        .chain(b.iter())
        .chain(c.iter())
        .copied()
        .collect();

    println!("{:?}", fruits); // ["apple", "banana", "cherry"]
}

zip:2つのイテレータをペアにする

zipは2つのイテレータを同時に走査し、それぞれの要素をタプルとして組み合わせます。短い方のイテレータが終わった時点で処理が止まります。

fn main() {
    let names = vec!["Alice", "Bob", "Carol"];
    let scores = vec![95, 80, 88];

    let result: Vec<(&str, i32)> = names.iter()
        .copied()
        .zip(scores.iter().copied())
        .collect();

    for (name, score) in &result {
        println!("{}: {}", name, score);
    }
    // Alice: 95
    // Bob: 80
    // Carol: 88
}

zipを使ったインデックス付き処理

enumerateの代替として、インデックスのイテレータとzipを組み合わせることもできます。より明示的な制御が必要な場合に便利です。

fn main() {
    let items = vec!["foo", "bar", "baz"];
    let indices = 1usize..;

    let indexed: Vec<(usize, &&str)> = indices
        .zip(items.iter())
        .collect();

    for (i, item) in &indexed {
        println!("{}: {}", i, item);
    }
    // 1: foo
    // 2: bar
    // 3: baz
}

flat_map:変換しながら平坦化する

flat_mapは各要素をイテレータに変換し、それらをすべて1つのイテレータに平坦化します。mapしたあとにflattenするのと同等ですが、1ステップで書ける点がシンプルです。

fn main() {
    let sentences = vec!["hello world", "foo bar baz"];

    let words: Vec<&str> = sentences.iter()
        .flat_map(|s| s.split_whitespace())
        .collect();

    println!("{:?}", words);
    // ["hello", "world", "foo", "bar", "baz"]
}

ネストしたVecの展開

flat_mapはネストしたコレクションを展開するのにも非常に便利です。

fn main() {
    let nested = vec![
        vec![1, 2, 3],
        vec![4, 5],
        vec![6, 7, 8, 9],
    ];

    let flat: Vec<i32> = nested.iter()
        .flat_map(|v| v.iter().copied())
        .collect();

    println!("{:?}", flat);
    // [1, 2, 3, 4, 5, 6, 7, 8, 9]
}

アダプタを組み合わせた実践例

これら3つのアダプタを組み合わせると、より複雑なデータ処理も簡潔に書けます。

fn main() {
    let team_a = vec!["Alice", "Bob"];
    let team_b = vec!["Carol", "Dave"];

    // 全メンバーを結合して番号を振り、名前を大文字にする
    let result: Vec<(usize, String)> = team_a.iter()
        .chain(team_b.iter())
        .enumerate()
        .map(|(i, name)| (i + 1, name.to_uppercase()))
        .collect();

    for (no, name) in &result {
        println!("No.{}: {}", no, name);
    }
    // No.1: ALICE
    // No.2: BOB
    // No.3: CAROL
    // No.4: DAVE
}

よくある注意点

遅延評価を意識する

Rustのイテレータは遅延評価(lazy evaluation)です。collectfor_eachなど、最終的な消費操作が呼ばれるまで実際の処理は実行されません。これはパフォーマンス上のメリットですが、副作用を期待するコードでは意図しない動作になることがあります。

fn main() {
    let v = vec![1, 2, 3];

    // これは何も出力されない(consumeされていない)
    let _iter = v.iter().map(|x| {
        println!("{}", x); // 実行されない!
        x * 2
    });

    // collect()して初めて実行される
    let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
    println!("{:?}", doubled); // [2, 4, 6]
}

まとめ

アダプタ用途
chain複数のイテレータを順番に連結する
zip2つのイテレータをペア(タプル)にする
flat_map変換と平坦化を同時に行う

これらのアダプタを使いこなすことで、明示的なループを減らし、意図が伝わりやすいコードを書けるようになります。Rustのイテレータはゼロコスト抽象化の代表例でもあり、パフォーマンスを犠牲にせずに表現力を高められる点が大きな魅力です。ぜひ積極的に活用してみてください。

← 記事一覧に戻る