【Rust入門】Iterator のアダプタ(map・filter など)を分かりやすく解説

Rust の イテレータ(Iterator)に用意されているイテレータアダプタの基本を初心者にも分かりやすく解説します。
イテレータアダプタとは
イテレータ(Iterator)とは、値の列を生成する値で、要素を順番に取り出すことができる仕組みです。イテレータの基本は「イテレータ(Iterator)の基本を分かりやすく解説」を参考にしてください。この記事で、基本となる next や into_iter などについて解説しています。
イテレータには、要素を順番に取り出すだけでなく、要素を変換したり、条件に合うものだけを残したりする便利なメソッドが多数用意されています。これらをイテレータアダプタと言います。イテレータアダプタは、既存のイテレータをもとに、新しいイテレータを作り出します。
イテレータアダプタの重要な点は、その場ですぐに最終結果を作るのではなく、新しいイテレータを返すという点です。この仕組みは「遅延評価」と呼ばれ、要素が実際に取り出されるときに初めて処理が実行されます。そのため、必要な分だけ効率よく処理を行うことができます。
また、イテレータアダプタには、変換などの処理内容を指定するためにクロージャをよく使用します。なお、クロージャの代わりに通常関数を渡すことも可能ですが、外部変数を利用できる点からクロージャがよく使用されます。
この記事では、代表的なイテレータアダプタを紹介しつつ、イテレータアダプタの基本的な使い方を紹介していきます。
代表的なイテレータアダプタ
ここでは、基本としてよく使用するイテレータアダプタである map と filter を中心に使い方を見ていきましょう。
map:各要素に処理を適用し、別の値に変換する
map は、イテレータの各要素に対して指定した処理を適用し、その結果となるイテレータに変換するアダプタです。
fn main() {
// 元データのベクタを作成
let numbers = vec![1, 2, 3];
// map を使用し、各要素を2倍にしたイテレータを作成
// (この時点ではまだ計算されない)
let doubled_iter = numbers.iter().map(|x| x * 2);
// イテレータをベクタに収集 (ここで初めて計算が行われる)
let result: Vec<_> = doubled_iter.collect();
// 結果を表示
println!("{:?}", result);
}【実行結果】 [2, 4, 6]
map では、引数にクロージャ(または関数)を受け取りイテレータを返却します。上記で doubled_iter はイテレータです。この時点では、各要素を 2 倍にするという計算はまだされていません。collect や for 文で要素を取り出すときに、初めて計算が実行されます。これが「遅延評価」です。
result 変数に束縛する際に使用している collect という処理は、イテレータの要素をコレクションにまとめるメソッドです。collect は戻り値の型が一意には決まりません。例えば、Vec や HashSet などのさまざまなコレクションに変換できるためです。このため「let result: Vec<_>」のように型注釈をつけるか、collect::<Vec<_>>() のように、どの型に変換するかを明示する必要があります
なお、上記では map によるイテレータ生成から、collect による遅延評価での処理という流れを理解してもらうために丁寧に文を分けて記載しましたが、以下のように「.(ドット)」で処理を連結させることで簡潔に記載も可能です。以降では、この形式で紹介していきます。
fn main() {
// 元データのベクタを作成
let numbers = vec![1, 2, 3];
// map を使用し、各要素を2倍にした結果を取得する
let result: Vec<_> = numbers.iter().map(|x| x * 2).collect();
// 結果を表示
println!("{:?}", result);
}filter:条件に一致する要素だけを残す
filter は、イテレータの各要素に対して条件による判定をし、条件に一致する要素だけを残したイテレータに変換するアダプタです。
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// filter を使用し、偶数のみを残す
let even_numbers: Vec<_> = numbers.iter().filter(|x| **x % 2 == 0).collect();
// 結果を表示
println!("{:?}", even_numbers);
}【実行結果】 [2, 4, 6, 8, 10]
filter では、クロージャが true を返した要素のみ残り、false の要素は除外されます。
例では、iter() を使用しているため要素は参照として扱われます。また、filter は要素をクロージャに渡して条件判定を行うため、この例では **x のように参照を外して値を取り出しています。(参照が重なっているため、2 回参照を外しています。)
map と filter を組み合わせて使う
イテレータアダプタは、「.(ドット)」でつなげることで複数の処理を連続して適用することで本領を発揮します。以下は、filter で偶数のみ残し、さらに map で 2 倍する例です。
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// filter と map を組み合わせて、偶数の2倍を計算する
let even_doubles: Vec<_> = numbers
.iter()
.filter(|x| **x % 2 == 0)
.map(|x| x * 2)
.collect();
// 結果を表示
println!("{:?}", even_doubles);
}【実行結果】 [4, 8, 12, 16, 20]
このように、イテレータアダプタを組み合わせて、データの処理を上から順に記述していくことで、可読性が高いコードを記述できることが特徴です。
その他の便利なイテレータアダプタ
map と filter はイテレータアダプタとして中心的なものです。他にも多くのイテレータアダプタがありますが、ここではよく使われるものをいくつか紹介します。
filter_map:変換を行い、成功(Option の Some)のみを残す
filter_map は、変換と絞り込みを同時に行うことができるアダプタで、変換を行い成功した値のみを残すことができます。
fn main() {
let values = vec!["10", "abc", "20", "def", "30"];
// filter_map を使用して、文字列を整数に変換し、成功したものだけを収集する
let numbers: Vec<_> = values
.iter()
.filter_map(|s| s.parse::<i32>().ok())
.collect();
// 結果を表示
println!("{:?}", numbers);
}【実行結果】 [10, 20, 30]
この例では、文字列の Vec を整数に変換しています。指定したクロージャは、整数に変換できた場合は、Option 型の Some(v) を返し、変換できなかった場合は None を返します。
結果を見ると分かるように filter_map は、変換結果が Some(v) であれば中の値 v を結果に残し、None の場合はその値を除外します。
flat_map:各要素を複数の要素に展開し、1 つの列としてつなげる
flat_map は、各要素を複数の要素に展開し、1 つの列としてつなげるアダプタです。
fn main() {
let numbers = vec![1, 2, 3];
let result: Vec<_> = numbers.iter().flat_map(|x| vec![*x, *x * 10]).collect();
// 結果を表示
println!("{:?}", result);
}【実行結果】 [1, 10, 2, 20, 3, 30]
この例で、指定したクロージャは与えられたイテレータの要素の値に対して、値と 10 倍の値のペアを生成しています。[1, 2, 3] がクロージャにより [[1, 10], [2, 20], [3, 30]] に展開され、flat_map により [1, 10, 2, 20, 3, 30] に 1 つの列につなげられているイメージを持ってもらうと分かりやすいかと思います。
その他のイテレータアダプタ一覧
上記で紹介したイテレータアダプタの他にも多くのイテレータアダプタがありますが、ここでは、代表的なものを一覧で簡単に紹介します。
| イテレータアダプタ | 概要 |
|---|---|
flatten | ネストした構造を平坦化する |
take | 先頭から指定した数だけ取得する |
take_while | 条件を満たす間だけ先頭から取得する |
skip | 先頭から指定した数だけをスキップする |
skip_while | 条件を満たす間だけ先頭からスキップする |
enumerate | インデックス付きで取得する |
zip | 複数のイテレータを組み合わせて取得する(短い方にあわせて終了する) |
まとめ
Rust の イテレータに用意されているイテレータアダプタの基本について解説しました。
イテレータアダプタは、既存のイテレータから新しいイテレータを作る仕組みです。代表的なイテレータアダプタである map と filter を用いて基本的な使い方を紹介しています。また、その他のイテレータアダプタについても簡単に紹介しました。
イテレータアダプタは、組み合わせて使用することで、処理の流れを非常に分かりやすく記述することができます。まずは、map と filter でしっかりと理解してもらうことが重要です。その上で、filter_map や flat_map などのさまざまなアダプタも徐々に使えるようになっていってください。
上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。







