はじめに
RustのIteratorトレイトには、データを効率よく処理するための多彩なアダプタが用意されています。以前の記事でもmapやfilterを取り上げましたが、今回はさらに一歩進んでchain・zip・flat_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)です。collectやfor_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 | 複数のイテレータを順番に連結する |
zip | 2つのイテレータをペア(タプル)にする |
flat_map | 変換と平坦化を同時に行う |
これらのアダプタを使いこなすことで、明示的なループを減らし、意図が伝わりやすいコードを書けるようになります。Rustのイテレータはゼロコスト抽象化の代表例でもあり、パフォーマンスを犠牲にせずに表現力を高められる点が大きな魅力です。ぜひ積極的に活用してみてください。