導入
「Vecの各要素を2倍にする」「条件に合う要素だけ残す」——こういう処理を毎回 for ループと空の Vec を用意して書くのは冗長です。Rust では、こうした変換を鎖のようにつなげる書き方が用意されています。前のレッスンで学んだクロージャが、ここで本領を発揮します。
説明
Vec や範囲(1..=5 など)はイテレータとして扱え、map・filter・collect といったアダプタをつなげて処理を組み立てられます。
fn main() {
let squares: Vec<i32> = (1..=5).map(|x| x * x).collect();
println!("{:?}", squares);
let evens: Vec<i32> = squares.into_iter().filter(|x| x % 2 == 0).collect();
println!("{:?}", evens);
}
.map(|x| x * x)… 各要素にクロージャを適用し、変換した新しい要素の列を作ります。.filter(|x| x % 2 == 0)… クロージャがtrueを返す要素だけを残します。.collect()… 変換し終わった要素を、指定した型(ここではVec<i32>)にまとめます。collectはどんな型に集めるか自分では決められないため、let squares: Vec<i32> = ...のように受け取り側に型注釈を書いて教えます(::<Vec<i32>>()のような書き方をせず、こちらの形が読みやすくて実務でもよく使われます)。
ここで大事なのは、map や filter は呼んだ瞬間には何も実行されないということです。これを遅延評価と呼びます。collect・sum・for_each のように「結果を取り出す(消費する)」メソッドを最後に呼んだときに、初めてパイプライン全体が実行されます。無駄な中間 Vec を作らず、必要な分だけ計算が進む——これも Rust の「ゼロコスト抽象化」の一例です。
他にもよく使うアダプタがあります。
fn main() {
let v = vec![1, 2, 3, 4, 5];
// sum: 合計。fold: 好きな初期値・演算で畳み込む
let total: i32 = v.iter().sum();
let product = v.iter().fold(1, |acc, x| acc * x);
println!("{} {}", total, product);
// enumerate: 添字付きで、for_each: 消費しながら1つずつ処理
let fruits = vec!["apple", "banana", "cherry"];
fruits.iter().enumerate().for_each(|(i, name)| {
println!("{}: {}", i, name);
});
}
sum()… 合計を計算して1つの値にまとめます(結果を代入する変数の型注釈が必要です)。fold(初期値, |acc, x| ...)…acc(これまでの累積値)とx(今の要素)から次のaccを作る、というのを繰り返します。sumの一般化版です。enumerate()…(添字, 値)のペアに変換します。for_each(|x| ...)… 各要素に対して処理を実行するだけで、値は集めません(forループのメソッド版)。
このほかにも find(条件に合う最初の要素)、any/all(条件を満たす要素が1つでもある/すべて満たす)、zip(2つを組にする)、rev(逆順にする)、take/skip(先頭を取る/飛ばす)、count(個数を数える)などがあり、組み合わせて使います。
やってみよう
下のエディタで map の中の式や filter の条件を書き換えて、パイプラインの結果がどう変わるか確かめてみましょう。sum や fold も試してみてください。
演習
1..=10 の範囲から、filter で偶数だけを取り出し、map でそれぞれを2倍にし、最後に sum で合計を求めて println! で出力してください。
ヒント1を見る
(1..=10).filter(|x| x % 2 == 0) で偶数(2, 4, 6, 8, 10)だけが残ります。
ヒント2を見る
続けて .map(|x| x * 2) で2倍にし、let total: i32 = ....sum(); の形で合計を受け取ってから println!("{}", total); します。