イテレーターとクロージャでデータを変換する

RustBeginner
オンラインで実践に進む

はじめに

コレクションを処理する場合、明示的な for ループが最も分かりやすい方法になることがよくあります。Rust にはイテレーターも用意されています。イテレーターは一度に 1 つの要素を生成し、読みやすいデータ処理パイプラインとして組み合わせられます。

この実験では、動作するループを基準として残し、iter と sum で同じ合計を求めます。さらに、小さなクロージャを作成し、map で読み取り値を変換し、filter で無効な読み取り値を除外し、最後にこれらの処理を組み合わせます。すべての編集は準備済みの Cargo プロジェクトで行うため、新しい Rust の概念に集中できます。

イテレーターでループの合計を再現する

このステップでは、用意されている for ループと、同じ合計を計算するイテレーターを比較します。

プロジェクトは /home/labex/project/reading-pipeline にあり、プログラムは src/main.rs にあります。用意されている vec![12, 18, -1, 24, 30] は、これらの値を含むベクターを簡潔に作成する vec! マクロです。目的としては、前の Vec Lab で行ったように、空のベクターを作成して各値を追加する方法と同じです。既存のループは &readings でベクターを借用し、各 &i32 を順に処理し、*reading で参照先の整数にアクセスします。

プロジェクトに移動し、基準となるプログラムを実行します。

cd /home/labex/project/reading-pipeline
cargo run --quiet
Loop total: 83

この合計には -1 を含むすべての読み取り値が含まれています。次に、ソースコードを開きます。

nano src/main.rs

Step 1 のコメントを次のコードに置き換えます。

    let iterator_total: i32 = readings.iter().sum();
    println!("Iterator total: {iterator_total}");

readings.iter() は、ベクターの各要素を借用するイテレーターを作成します。ベクターから要素を削除したり、ベクターを消費したりすることはありません。sum() は生成された要素を繰り返し加算します。Rust の標準ライブラリは、借用された整数を加算する方法を認識しています。: i32 という型注釈は、sum が作成する結果の型を指定します。

Ctrl+O で保存し、Enter を押してから、Ctrl+X で終了します。実行する前にコードを確認します。

cargo check
cargo run --quiet
Loop total: 83
Iterator total: 83

2 つの行が一致していれば、ループとイテレーターが同じ計算を表していることが分かります。iter() はベクターを借用しただけなので、ベクターは後のステップでも引き続き使用できます。

クロージャと map で読み取り値を変換する

このステップでは、クロージャを定義し、map を使ってすべての読み取り値に適用します。

クロージャは、変数に格納したり、イテレーターアダプターに直接渡したりできる、小さな名前のない関数です。|parameter: Type| expression という形式では、縦棒の間にパラメーターを記述し、式の値を返します。ここでは、調整用のクロージャでオフセットとして 2 を加えます。

ソースコードを開きます。

nano src/main.rs

Step 2 のコメントを次のコードに置き換えます。

    let add_offset = |reading: i32| reading + 2;
    let adjusted: Vec<i32> = readings.iter().map(|reading| add_offset(*reading)).collect();
    println!("Adjusted all: {adjusted:?}");

map はイテレーターの各要素に対してクロージャを 1 回呼び出し、クロージャが返した値を生成します。型の流れは、iter() からの &i32 → *reading でコピーされた i32 → add_offset で調整された i32 → collect() で作られる Vec<i32> の要素、となります。:? フォーマッターは、ベクターを読みやすいデバッグ形式で出力します。

保存して終了し、確認してから実行します。

cargo check
cargo run --quiet

新しく追加された最後の行は、次のようになるはずです。

Adjusted all: [14, 20, 1, 26, 32]

無効な -1 も 1 に変換されています。map は要素を変換しますが、どの要素を残すかは判断しません。

filter で有効な読み取り値だけを残す

このステップでは、filter を使って 0 以上の読み取り値だけを残し、元のベクターは変更しません。

イテレーターの filter アダプターは、候補ごとに判定用クロージャを呼び出します。判定結果が true なら要素を残し、false なら除外します。ソースコードを開きます。

nano src/main.rs

Step 3 のコメントを次のコードに置き換えます。

    let valid: Vec<i32> = readings
        .iter()
        .copied()
        .filter(|reading| *reading >= 0)
        .collect();
    println!("Valid only: {valid:?}");

メソッドチェーンは、ドットの後で改行し、インデントした行に続けて記述できます。iter() はベクターを借用し、copied() は借用された各 &i32 要素を i32 に変換します。整数は Copy を実装しているため、この処理のコストは小さくなります。続いて filter の判定用クロージャは、各 i32 候補への一時的な参照を受け取ります。要素を消費せずに調べるため、比較では *reading を使用します。型の流れは、&i32 → コピーされた i32 → 残された i32 → Vec<i32> です。

保存して終了し、実行します。

cargo run --quiet

最後の行は、次のようになるはずです。

Valid only: [12, 18, 24, 30]

負の値がなくなり、残った値は元の順序を保っています。

完全な処理パイプラインを組み立てる

このステップでは、フィルタリングとマッピングを連結し、処理済みの読み取り値を収集して、その合計を計算します。

イテレーターアダプターは遅延評価されます。filter と map は処理内容を定義しますが、結果を要求する消費操作が行われるまで、要素は処理を通過しません。collect と sum は消費操作です。これらはイテレーターパイプラインを消費しますが、借用元のベクターは消費しません。

ソースコードを開きます。

nano src/main.rs

Step 4 のコメントを次のコードに置き換えます。

    let valid_adjusted: Vec<i32> = readings
        .iter()
        .copied()
        .filter(|reading| *reading >= 0)
        .map(add_offset)
        .collect();
    let valid_total: i32 = valid_adjusted.iter().sum();
    println!("Valid adjusted: {valid_adjusted:?}");
    println!("Valid adjusted total: {valid_total}");

このパイプラインは、まず -1 を除外し、次に map(add_offset) で残った各 i32 を先ほど定義したクロージャに渡します。パラメーターと戻り値の型がアダプターに適合する場合は、クロージャの名前を直接指定できます。2 つ目のイテレーターは収集したベクターを借用し、その合計を計算します。

保存して終了し、確認してから実行します。

cargo check
cargo run --quiet

最後の 2 行は、次のようになるはずです。

Valid adjusted: [14, 20, 26, 32]
Valid adjusted total: 92

処理の順序が重要です。マッピングの前にフィルタリングすることで、無効な読み取り値が一見有効な正の値に変換されるのを防げます。元の readings ベクターは借用されただけなので、その後も使用できます。

まとめ

明示的なループとイテレーターを比較し、クロージャを作成して再利用しました。また、map で値を変換し、filter で値を選択し、新しいベクターを収集し、sum で合計を計算しました。さらに、各パイプラインを借用イテレーターから開始することで、元のベクターを保持しました。