Rustのイテレータ:遅延データ処理パイプライン
イテレータは、Rustにおける「遅延データ処理パイプライン」です。結果を即座に計算するのではなく、要素を1つずつ生成するため、呼び出しを連鎖させることで、データシーケンスを宣言的に処理することができます。
イテレータは、工場の組立ラインのようなものです。データは一方の端から入力され、一連の処理(フィルタリング、変換、抽出、集計)を経て、最終的にもう一方の端から完成品として出力されます。各処理は1つのことしか行いませんが、それらを組み合わせることで、複雑な処理タスクを遂行することができます。
1. 組立ライン工場の物語
(1) 問題点:ループを使ったデータ処理は面倒で時間がかかる
シャオ・ミンは、ラスト工場の組立ラインの監督者です。彼は、あるバッチの部品データを処理する必要があります:
- 条件を満たすすべての製品(偶数番号の品目)を選別する
- 各部分を2回ずつ印をつける
- 最初の5つだけを取る
- 統計の集計値
彼は、伝統的なforループを使ってこれを書いた:
fn main() {
let parts = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut result = vec![];
let mut count = 0;
for &part in &parts {
if part % 2 == 0 { // Steps1: Filter
let doubled = part * 2; // Steps2: Convert
result.push(doubled);
count += 1;
if count == 5 { // Steps3: Excerpt
break;
}
}
}
let sum: i32 = result.iter().sum();
println!("Result: {:?}, Sum: {}", result, sum);
}
コードは動作しますが、ロジックがプログラム全体に散在しています。もし要件が「最初の2つを再びスキップする」や「再び偶数のアイテムを取り出す」といったものに変更された場合、ループ全体を書き直さなければならなくなります。
(2) イテレータ・パイプライン・アプローチ
上記のロジックを、イテレータの連鎖呼び出しを使って書き直してください:
fn main() {
let parts = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let sum: i32 = parts.iter()
.filter(|&&n| n % 2 == 0) // Steps1: Filter for Even Numbers
.map(|&n| n * 2) // Steps2: multiply by2
.take(5) // Steps3: Take the first 5
.sum(); // Steps4: Sum
println!("Sum: {}", sum);
}
コードは「どのように」から「何を」へと重点が移りました。各行は独立したプロセスを表しており、組み立てラインの作業台を調整するように、いつでも挿入、削除、または順序変更が可能です。
2. 概念図
以下のマーメイド図は、iter() から collect() に至るまでの、Iterator トレイトのメソッドチェーンにおける遅延評価の全プロセスを示しています:
graph LR
A["Raw Data<br/>1..=20"] --> B["iter()<br/>Create an iterator"]
B --> C["filter(|n| n%2==0)<br/>Filter for Even Numbers"]
C --> D["map(|n| n*3)<br/>Each element ×3"]
D --> E["skip(2)<br/>Skip the first 2"]
E --> F["take(5)<br/>Take the first 5"]
F --> G["collect()<br/>Consumer: Triggered Evaluation"]
H["Lazy Evaluation: Build the pipeline only<br/>Do not calculate immediately"] -.-> C
H -.-> D
H -.-> E
H -.-> F
I["Consumer-Driven Execution<br/>Produce the final result"] -.-> G
G --> J["Results<br/>[18, 24, 30, 36, 42]"]
3. 学習内容
- イテレータ・トレイトと
nextメソッド:イテレータの中核となる契約。next()がどのように要素を1つずつ生成するのかを理解する - イテレータアダプタ:
map/filter/take/skip/chainおよびその他の変換方法 - コンシューマー:
collect/sum/count/foldなど――イテレータの実行を駆動するメソッド - 遅延評価:アダプタは直ちに実行されず、コンシューマが呼び出されるまで待機します。
- カスタムイテレータ:独自の型用に
Iterator traitを実装してください - チェーンコールの実践ガイド:アダプターとコンシューマーを組み合わせてデータ処理タスクを完了させる
4. 基本概念
graph TB
A[Iterator Iterator] --> B[Iterator Adapters<br>Adapter]
A --> C[Consumer Consumer]
B --> D["map(|x| x+1) Convert"]
B --> E["filter(|x| x>0) Filter"]
B --> F["take(n) Take the first n"]
B --> G["skip(n) Skip n"]
B --> H["chain(other) Concat"]
B --> I["enumerate() Add an index"]
C --> J["collect() Collected into a set"]
C --> K["sum() Sum"]
C --> L["count() Count"]
C --> M["fold(init, fn) Collapse"]
C --> N["for x in iter Loop"]
B -.->|"❌ Inertia<br>If it isn't called, it won't run."| C
C --> O["Triggered Evaluation"]
(1) イテレータ・アダプタとコンシューマ
| 機能 | イテレータ・アダプタ | コンシューマ |
|---|---|---|
| 機能 | イテレータを変換する(あるイテレータから別のイテレータへ) | イテレータを処理し、最終結果を生成する |
| 戻り値 | Iterator の新しい型 |
具体的な値(例:Vec<T>、i32、usize) |
| 遅延評価 | 遅延評価(即時実行されない) | 貪欲評価(即時実行される) |
| 代表的な手法 | map, filter, take, skip, chain |
collect, sum, count, fold, for_each |
| チェーンの位置 | 中間ステップ | 最終ステップ |
| 例 | `.map( | x |
(2) 一般的なイテレータ・アダプタのクイックリファレンス
| アダプター | 機能 | 例 | 結果 |
|---|---|---|---|
map(f) |
各要素にf変換を適用する | [1,2,3].iter().map(|x| x*2) |
2, 4, 6 |
filter(p) |
条件 p を満たす要素を残す | [1..=5].filter(|x| x%2==0) |
2, 4 |
take(n) |
最初の n 個の要素のみを取り出す | [1..].take(3) |
1, 2, 3 |
skip(n) |
最初の n 個の要素をスキップ | [1..=5].skip(2) |
3, 4, 5 |
chain(it) |
別のイテレータを追加 | [1,2].iter().chain([3,4].iter()) |
1, 2, 3, 4 |
enumerate() |
各要素にインデックスを割り当てる (i, val) |
['a','b'].iter().enumerate() |
(0,'a'), (1,'b') |
zip(it) |
2つのイテレータを1つずつペアにする | [1,2].iter().zip(['a','b'].iter()) |
(1,'a'), (2,'b') |
rev() |
逆イテレータ | [1..=3].rev() |
3, 2, 1 |
(3) 一般的なコンシューマメソッドのクイックリファレンス
| コンシューマー | 効果 | 例 | 戻り値の型 | 貪欲法か |
|---|---|---|---|---|
collect() |
セットで収集 | iter.collect::<Vec<_>>() |
B: FromIterator |
はい |
sum() |
合計 | iter.sum::<i32>() |
S: Sum |
はい |
count() |
件数 | iter.count() |
usize |
はい |
fold(init, f) |
累積計算 | iter.fold(0, |acc, x| acc + x) |
初期値の種類 | はい |
reduce(f) |
初期値なしでの折りたたみ | iter.reduce(|a, b| a + b) |
Option<Item> |
はい |
for_each(f) |
1つずつ実行(戻り値なし) | iter.for_each(|x| println!(x)) |
() |
はい |
any(p) |
一致するものはありますか? | iter.any(|x| x > 0) |
bool |
はい |
all(p) |
すべての条件は満たされていますか? | iter.all(|x| x > 0) |
bool |
はい |
find(p) |
条件に合致する最初のものを探す | iter.find(|x| *x > 3) |
Option<Item> |
はい |
max() / min() |
最大/最小 | iter.max() |
Option<Item> |
はい |
5. 例
(1) ▶ サンプル:Iterator トレイトと next() メソッド――イテレータの本質を理解する(難易度 ⭐)
// ============================================
// Manual Invocation next() Understanding How Iterators Work
// ============================================
fn main() {
let numbers = vec![10, 20, 30, 40, 50];
// iter() Returns an iterator,Does not consume vectors
let mut iter = numbers.iter();
// next() Every time it returns Option<&T>
// Some(&value) Indicates that there is another element
// None Indicates the end of the iteration
println!("{:?}", iter.next()); // Some(10)
println!("{:?}", iter.next()); // Some(20)
println!("{:?}", iter.next()); // Some(30)
println!("{:?}", iter.next()); // Some(40)
println!("{:?}", iter.next()); // Some(50)
println!("{:?}", iter.next()); // None
println!("{:?}", iter.next()); // None (A subsequent call still returns None)
// for A loop is next() syntactic sugar
let mut count = 0;
let iter2 = numbers.iter();
for val in iter2 {
println!("for loop #{}: {}", count, val);
count += 1;
}
}
出力:
Some(10)
Some(20)
Some(30)
Some(40)
Some(50)
None
None
for loop #0: 10
for loop #1: 20
for loop #2: 30
for loop #3: 40
for loop #4: 50
イテレータの中核となる契約は
next()メソッドです。このメソッドは呼び出されるたびにSome(element)を返し、要素が尽きた際にはNoneを返します。forループは、Noneに遭遇するまでnext()を繰り返し呼び出すための構文糖衣です。これを理解すれば、すべてのイテレータの基礎を理解したことになります。
(2) ▶ サンプル:イテレータ・アダプタの連鎖 — map / filter / take / skip (難易度: ⭐⭐)
// ============================================
// Combine Multiple Adapters to Build a Data Processing Pipeline
// ============================================
fn main() {
// Raw Data: 1 to 20
let data = 1..=20;
// Pipeline: Filter for Even Numbers → multiply by 3 → Skip the first 2 → Take the first 5
let result: Vec<i32> = data
.filter(|&n| n % 2 == 0) // [2,4,6,8,10,12,14,16,18,20]
.map(|n| n * 3) // [6,12,18,24,30,36,42,48,54,60]
.skip(2) // [18,24,30,36,42]
.take(5) // [18,24,30,36,42]
.collect(); // Triggered Evaluation,Collected Vec
println!("Pipeline result: {:?}", result);
// Another pipeline: use chain to Concat two slices
let first = vec!["A", "B", "C"];
let second = vec!["X", "Y", "Z"];
let combined: Vec<&str> = first.iter()
.chain(second.iter())
.copied()
.collect();
println!("Chained: {:?}", combined);
// Use enumerate to index elements
let fruits = vec!["apple", "banana", "cherry"];
let indexed: Vec<(usize, &str)> = fruits.iter()
.enumerate()
.map(|(i, &name)| (i + 1, name))
.collect();
println!("Indexed: {:?}", indexed);
}
出力:
Pipeline result: [18, 24, 30, 36, 42]
Chained: ["A", "B", "C", "X", "Y", "Z"]
Indexed: [(1, "apple"), (2, "banana"), (3, "cherry")]
アダプタチェーンは、組立ライン上の作業ステーションの配置のようなものです。各アダプタは1つの処理のみを行い、データは各作業ステーションを順次通過していきます。
collect()はパイプラインの末端にある要求信号であり、これがなければ、パイプライン内の作業者は作業を開始しません(遅延評価)。
(3) ▶ サンプル:カスタムイテレータ — 独自の型に対して Iterator トレイトを実装する (難易度: ⭐⭐)
// ============================================
// Custom Fibonacci Iterator
// Implementation Iterator trait Make any type iterable
// ============================================
// Fibonacci Sequence Generator
struct Fibonacci {
current: u64,
next: u64,
max: u64,
}
impl Fibonacci {
fn new(max: u64) -> Self {
Fibonacci {
current: 0,
next: 1,
max,
}
}
}
// Implementation Iterator trait It is the core of custom iterators.
impl Iterator for Fibonacci {
// Item The type of the elements returned by the iterator
type Item = u64;
// next() Back Option<Self::Item>
// Some(value) Indicates that there is another element
// None Indicates the end of the iteration
fn next(&mut self) -> Option<Self::Item> {
if self.current > self.max {
return None;
}
let result = self.current;
// Update to the next Fibonacci number
let new_next = self.current + self.next;
self.current = self.next;
self.next = new_next;
Some(result)
}
}
fn main() {
println!("Fibonacci up to 50:");
let fib = Fibonacci::new(50);
// Fibonacci It can now be used in for In a loop
for (i, n) in fib.enumerate() {
println!(" fib({}) = {}", i, n);
}
// It can also be used in conjunction with an adapter chain
println!("\nEven Fibonacci numbers up to 100:");
let even_fibs: Vec<u64> = Fibonacci::new(100)
.filter(|&n| n % 2 == 0)
.collect();
println!("{:?}", even_fibs);
}
出力:
Fibonacci up to 50:
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
Even Fibonacci numbers up to 100:
[0, 2, 8, 34]
Iterator traitを実装するには、たった1つのこと、すなわちtype Item(要素型)とfn next()(生成規則)を定義するだけで済みます。実装が完了すると、その型には自動的にすべてのアダプタメソッド(map、filter、takeなど)が追加されます。これは Rust における「ダックタイピング」の概念の具現化です。つまり、next()を実装すれば、それをイテレータと同じように使用できるようになります。
(4) ▶ サンプル:実践的な活用例――折りたたむ/足し合わせる/数える/集める(難易度 ⭐⭐⭐)
// ============================================
// Consumer:Drive the iterator to execute and produce the final result
// ============================================
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// sum() — Sum
let total: i32 = numbers.iter().sum();
println!("Sum: {}", total);
// count() — Count
let cnt = numbers.iter().count();
println!("Count: {}", cnt);
// fold() — General Folding Operation (initial value + accumulator closure)
// Here, we calculate 10!
let factorial: u64 = (1..=10u64).fold(1, |acc, x| acc * x);
println!("10! = {}", factorial);
// fold() — Manual Implementation of sum and count
let sum_via_fold: i32 = numbers.iter().fold(0, |acc, &x| acc + x);
let count_via_fold: usize = numbers.iter().fold(0, |acc, _| acc + 1);
println!("Sum (via fold): {}", sum_via_fold);
println!("Count (via fold): {}", count_via_fold);
// collect() — Collected various types of collections
let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();
println!("Doubled: {:?}", doubled);
let even_set: std::collections::HashSet<i32> = numbers.iter()
.filter(|&&x| x % 2 == 0)
.copied()
.collect();
println!("Even set: {:?}", even_set);
// General: find the sum of the first 5 even squares
let complex_result: i32 = (1..=100)
.filter(|&n| n % 2 == 0)
.map(|n| n * n)
.take(5)
.fold(0, |acc, n| acc + n);
println!("Sum of first 5 even squares: {}", complex_result);
}
出力:
Sum: 55
Count: 10
10! = 3628800
Sum (via fold): 55
Count (via fold): 10
Doubled: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
Even set: {2, 4, 6, 8, 10}
Sum of first 5 even squares: 220
コンシューマーはパイプラインの末端にある重要な構成要素です。
sum()、count()、およびfold()は数値結果を直接計算し、collect()はデータをセットに収集します。fold()は最も汎用性の高いコンシューマーであり、sum()とcount()は本質的にfold()の特殊な形態です。パイプラインが実際に実行されるためにはコンシューマーが不可欠であり、そうでなければ、すべては単なる空論に過ぎません。
(5) ▶ サンプル:総合演習 — カスタムイテレータを実装して FizzBuzz ジェネレータを作成する(難易度 ⭐⭐⭐)
// ============================================
// Comprehensive Example:Custom Iterators + Adapter Chain
// ============================================
struct FizzBuzz {
current: u32,
limit: u32,
}
impl FizzBuzz {
fn new(limit: u32) -> Self {
FizzBuzz { current: 0, limit }
}
}
impl Iterator for FizzBuzz {
type Item = String;
fn next(&mut self) -> Option<String> {
self.current += 1;
if self.current > self.limit {
return None;
}
let n = self.current;
let result = match (n % 3, n % 5) {
(0, 0) => "FizzBuzz".to_string(),
(0, _) => "Fizz".to_string(),
(_, 0) => "Buzz".to_string(),
_ => n.to_string(),
};
Some(result)
}
}
struct Fibonacci {
curr: u64,
next: u64,
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let result = self.curr;
self.curr = self.next;
self.next = result + self.next;
Some(result)
}
}
fn main() {
println!("=== FizzBuzz (1-20) ===");
for item in FizzBuzz::new(20) {
print!("{} ", item);
}
println!();
let fizz_count = FizzBuzz::new(100)
.filter(|s| s.starts_with("Fizz"))
.count();
println!("1-100 Fizz occurrences: {}", fizz_count);
println!("\n=== Fibonacci first 15 terms ===");
let fib = Fibonacci { curr: 0, next: 1 };
for val in fib.take(15) {
print!("{} ", val);
}
println!();
let fib_sum: u64 = Fibonacci { curr: 1, next: 1 }
.take_while(|&x| x < 1_000_000)
.filter(|&x| x % 2 == 0)
.sum();
println!("Fibonacci < 1M sum of even numbers: {}", fib_sum);
}
出力:
=== FizzBuzz (1-20) ===
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz
1-100 Fizz occurrences: 27
=== Fibonacci first 15 terms ===
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377
Fibonacci < 1M sum of even numbers: 1089154
カスタムイテレータを作成するには、
Iteratorトレイトのnext()メソッドを実装するだけです。FizzBuzzジェネレータはfilter/countを使用してチェーン化できます。Fibonacci無限イテレータはtake/take_whileを使用して出力を制限し、その後filter/sumを使用して結果を集約することができます。
❓ よくある質問
iter()、into_iter()、iter_mut() の違いは何ですか?iter() は、所有権を譲渡せずに &T(不変参照)を返します。into_iter()はT(所有権の譲渡)を返し、元のコレクションを消費します;iter_mut()は&mut T(可変参照)を返し、要素の変更を許可します。forループはデフォルトでinto_iter()を使用します。collect()、sum() など)を呼び出したときに初めて動作を開始します。これは Rust のイテレータ設計の中核となる機能であり、実際に必要になったときにのみ計算を行う、オーバーヘッドゼロの抽象化です。collect() どの型を収集すべきか、どうすればわかりますか?.collect::<Vec<i32>>() を使用するか、変数の型を宣言して let v: Vec<i32> = iter.collect(); 指定します。コンパイラは、ターゲット型の実装に基づいて、収集方法を決定します FromIterator。type Item とは何ですか?Item は、next() によって返される Some の型を指定する関連型です。 たとえば、Iterator for Fibonacci 内の type Item = u64 は、next() が毎回 Option<u64> を返すことを示しています。関連型を使用すると、追加のジェネリックパラメータを指定することなく、イテレータが生成する要素の型を指定することができます。fold() と reduce() の違いは何ですか?fold() は初期値を必要としますが、reduce() は最初の要素を初期値として使用します。fold(0, \|acc, x\| acc + x) は 0 からカウントを開始し、reduce(\|acc, x\| acc + x) は最初の要素からカウントを開始します。fold()は常に指定した初期値の型を返しますが、reduce()はOption<Self::Item>を返します(イテレータが空の場合はNoneを返します)。📖 まとめ
- Iterator トレイトは、Rustのイテレータシステムの基盤となっています。実装する必要があるのは、
next()を返すOption<Self::Item>メソッドだけです。 - イテレータ・アダプタ (
map,filter,take,skip,chain) は遅延評価型であり、操作を記録するだけで、計算自体は行いません。 - コンシューマー (
collect,sum,count,fold) はパイプラインのエンドポイントであり、これらを呼び出すことで実際の計算が開始されます - 連鎖可能な呼び出しは、データ処理コードを「どのように行うか」(命令型)から「何をしたいか」(宣言型)へと変え、コードをより明確にし、組み合わせやすくします
- カスタムイテレータは、
Iterator traitを実装するだけで済みます。一度実装すれば、すべてのアダプタメソッドを自動的に利用できるようになります。 - 遅延評価は、イテレータの設計における中核となる原則であり、必要なときにのみ値を計算し、不必要な中間的な割り当てを回避する、オーバーヘッドゼロの抽象化です。
📝 練習問題
-
難易度 ⭐: イテレータを使用して、以下のコードを書き直してください。
[1, 2, 3, 4, 5, 6, 7, 8]からすべての奇数を抽出し、それらに 10 を掛けて Vec に格納し、出力してください。RUST// Replace this loop with a chain of iterators let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8]; let mut result = vec![]; for &n in &numbers { if n % 2 == 1 { result.push(n * 10); } } println!("{:?}", result); -
難易度 ⭐⭐:
struct StepRange { start: i32, end: i32, step: i32 }に対してIteratorトレイトを実装し、for n in StepRange::new(0, 10, 2)と同様に使用できるようにし、0、2、4、6、8、10 を出力するようにします。次に、mapを使用して各値を2乗し、collectを使用してそれらを Vec にまとめます。 -
難易度 ⭐⭐⭐: イテレータメソッドを使用して、あるテキスト中に各単語が何回出現するかを数える関数
fn word_count(text: &str) -> std::collections::HashMap<String, usize>を作成してください。要件:split_whitespace()を使用してテキストを分割し、mapを使用して小文字に変換し、foldを使用して HashMap を構築してください。ヒント:HashMapのentry()API をor_insert()と組み合わせると、カウントが簡単になります。



