404 Not Found

404 Not Found


nginx

Rustのイテレータ:遅延データ処理パイプライン

イテレータは、Rustにおける「遅延データ処理パイプライン」です。結果を即座に計算するのではなく、要素を1つずつ生成するため、呼び出しを連鎖させることで、データシーケンスを宣言的に処理することができます。

イテレータは、工場の組立ラインのようなものです。データは一方の端から入力され、一連の処理(フィルタリング、変換、抽出、集計)を経て、最終的にもう一方の端から完成品として出力されます。各処理は1つのことしか行いませんが、それらを組み合わせることで、複雑な処理タスクを遂行することができます。


1. 組立ライン工場の物語

(1) 問題点:ループを使ったデータ処理は面倒で時間がかかる

シャオ・ミンは、ラスト工場の組立ラインの監督者です。彼は、あるバッチの部品データを処理する必要があります:

  1. 条件を満たすすべての製品(偶数番号の品目)を選別する
  2. 各部分を2回ずつ印をつける
  3. 最初の5つだけを取る
  4. 統計の集計値

彼は、伝統的なforループを使ってこれを書いた:

RUST
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) イテレータ・パイプライン・アプローチ

上記のロジックを、イテレータの連鎖呼び出しを使って書き直してください:

RUST
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 トレイトのメソッドチェーンにおける遅延評価の全プロセスを示しています:

100%
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. 学習内容


4. 基本概念

100%
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>i32usize
遅延評価 遅延評価(即時実行されない) 貪欲評価(即時実行される)
代表的な手法 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() メソッド――イテレータの本質を理解する(難易度 ⭐)

RUST
// ============================================
// 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;
    }
}

出力:

TEXT
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 (難易度: ⭐⭐)

RUST
// ============================================
// 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);
}

出力:

TEXT
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 トレイトを実装する (難易度: ⭐⭐)

RUST
// ============================================
// 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);
}

出力:

TEXT
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()(生成規則)を定義するだけで済みます。実装が完了すると、その型には自動的にすべてのアダプタメソッド(mapfiltertake など)が追加されます。これは Rust における「ダックタイピング」の概念の具現化です。つまり、next() を実装すれば、それをイテレータと同じように使用できるようになります。


(4) ▶ サンプル:実践的な活用例――折りたたむ/足し合わせる/数える/集める(難易度 ⭐⭐⭐)

RUST
// ============================================
// 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);
}

出力:

TEXT
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 ジェネレータを作成する(難易度 ⭐⭐⭐)

RUST
// ============================================
// 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);
}

出力:

TEXT
=== 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 を使用して結果を集約することができます。


❓ よくある質問

Q iter()into_iter()iter_mut() の違いは何ですか?
A これらはそれぞれ異なる種類のイテレータを返します。iter() は、所有権を譲渡せずに &T(不変参照)を返します。into_iter()T(所有権の譲渡)を返し、元のコレクションを消費します;iter_mut()&mut T(可変参照)を返し、要素の変更を許可します。forループはデフォルトでinto_iter()を使用します。
Q アダプターチェーンはコードを一切実行していないのに、なぜエラーが発生しないのですか?
A アダプターは遅延評価されるためです。アダプターは「操作プラン」を構築するだけで、それを実行することはありません。これは、工場内のすべての機械が設置されているものの電源が入っていない状態に似ています。機械は、コンシューマー(collect()sum() など)を呼び出したときに初めて動作を開始します。これは Rust のイテレータ設計の中核となる機能であり、実際に必要になったときにのみ計算を行う、オーバーヘッドゼロの抽象化です。
Q collect() どの型を収集すべきか、どうすればわかりますか?
A 型推論によって判断します。ターゲット型を指定する必要があります。通常は、TurboFish構文:.collect::<Vec<i32>>() を使用するか、変数の型を宣言して let v: Vec<i32> = iter.collect(); 指定します。コンパイラは、ターゲット型の実装に基づいて、収集方法を決定します FromIterator
Q カスタムイテレータにおける type Item とは何ですか?
A Item は、next() によって返される Some の型を指定する関連型です。 たとえば、Iterator for Fibonacci 内の type Item = u64 は、next() が毎回 Option<u64> を返すことを示しています。関連型を使用すると、追加のジェネリックパラメータを指定することなく、イテレータが生成する要素の型を指定することができます。
Q fold()reduce() の違いは何ですか?
A fold() は初期値を必要としますが、reduce() は最初の要素を初期値として使用します。fold(0, \|acc, x\| acc + x) は 0 からカウントを開始し、reduce(\|acc, x\| acc + x) は最初の要素からカウントを開始します。fold()は常に指定した初期値の型を返しますが、reduce()Option<Self::Item>を返します(イテレータが空の場合はNoneを返します)。

📖 まとめ


📝 練習問題

  1. 難易度 ⭐: イテレータを使用して、以下のコードを書き直してください。[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);
    
  2. 難易度 ⭐⭐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 にまとめます。

  3. 難易度 ⭐⭐⭐: イテレータメソッドを使用して、あるテキスト中に各単語が何回出現するかを数える関数 fn word_count(text: &str) -> std::collections::HashMap<String, usize> を作成してください。要件:split_whitespace() を使用してテキストを分割し、map を使用して小文字に変換し、fold を使用して HashMap を構築してください。ヒント:HashMapentry() API を or_insert() と組み合わせると、カウントが簡単になります。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%