はじめに
コレクションを処理する場合、明示的な 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 で合計を計算しました。さらに、各パイプラインを借用イテレーターから開始することで、元のベクターを保持しました。


