【Rust入門】Iteratorの消費アダプタ(sum・fold・collect など)を分かりやすく解説

Rust の イテレータ(Iterator)に用意されている消費アダプタの基本を初心者にも分かりやすく解説します。
イテレータの消費とは
イテレータ(Iterator)とは、値の列を生成する値で、要素を順番に取り出すことができる仕組みです。イテレータの基本やイテレータアダプタについて理解が不十分な方は、以下にまとめていますので参考にしてください。
上記では、イテレータの基本とアダプタを使って別のイテレータを作る方法を紹介しました。
イテレータは、next を使って順に値を取り出すことができます。next を繰り返し呼び出すことで、イテレータの要素を最後まで(または途中まで)使い切ることを「消費」と言います。イテレータを消費しつつ、最終的な計算結果を得るメソッドを「消費アダプタ(consuming adapter)」と言います。
この記事では、代表的な消費アダプタを紹介しつつ、イテレータを消費する方法を紹介します。
代表的な消費アダプタ
以降では、よく使用される代表的な消費アダプタを紹介していきます。
collect:コレクションに集約する
collect は、イテレータの要素をコレクションにまとめることができる消費アダプタです。
Vec への集約
以下は、numbers を 2 倍した数値列を Vec に集約する例です。
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// map を適用したイテレータを collect して新しい Vec に集約
let result_vec: Vec<_> = numbers.iter().map(|x| x * 2).collect();
// 結果を表示
println!("{:?}", result_vec);
}【実行結果】 [2, 4, 6, 8, 10]
例で「numbers.iter().map(|x| x * 2)」は、map というイテレータアダプタを使い、各要素を 2 倍したイテレータを返します。この段階では、x * 2 の計算は適用されていません。その後、collect という消費アダプタが適用されることで計算が実行されて Vec の result_vec にまとめられます。
なお、collect は、任意のコレクションにまとめることができるため「result_vec: Vec<i32>」という型注釈をつけるか、「collect::<Vec<_>>() 」のようにどの型に変換するかを明示する必要があります。
その他のコレクションへの集約
上記で Vec への集約を紹介しましたが、任意のコレクションにまとめられると説明しました。1 例として HashSet に集約する例を見ておきましょう。
以下は、numbers の 値を 2 倍にした後、HashSet に集約しています。HashSet を用いると、重複を取り除いた集合として扱うことができます。
use std::collections::HashSet;
fn main() {
let numbers = vec![1, 2, 3, 1, 2, 5];
// HashSet を使用して、重複を排除したコレクションに集約
let result_set: HashSet<_> = numbers.iter().map(|x| x * 2).collect();
// 結果を表示
println!("{:?}", result_set);
}【実行結果例】※ HashSet は順序保証がされないため、順序は実行により異なります
{4, 2, 6, 10}型注釈で「: HashSet<_>」を付けることで、HashSet に集約できます。このように、collect は変換先のコレクションの型によって結果が変わることが特徴的です。
省略しますが、同様に HashMap などのその他コレクションも同様に扱うことができます。
無限のシーケンスの場合
イテレータの強力な特徴は、無限のデータシーケンスも扱うことができる点です。ただし、collect を使用する場合には、注意が必要です。以下のように take や take_while などで、データの範囲を制限して適用をする必要があります。
fn main() {
// 無限の数値列を生成するイテレータ
let infinite_numbers = 1..;
// 無限の数値列から最初の 5 個を collect して Vec に集約
let result_vec: Vec<_> = infinite_numbers.take(5).collect();
// 結果を表示
println!("{:?}", result_vec);
}【実行結果】 [1, 2, 3, 4, 5]
例では「1..」により、RangeFrom<i32> のイテレータを生成しています。このイテレータは 1 から順番に数値を取り出せますが、末尾が決まっていません。そのため、take により先頭 5 個を取り出すイテレータアダプタを適用してから collect を適用しています。
無限のデータシーケンスの場合は、消費アダプタの使用には注意が必要です。以降で紹介する消費アダプタについても同様に無限に続くイテレータの場合は注意して使用してください。
count:件数を取得する
count は、イテレータの要素数を取得する消費アダプタです。count はイテレータを消費し、要素を最後まで走査することで件数を返します。
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// count() メソッドを使用して、イテレータの要素数を数える
let count = numbers.iter().count();
// 結果を表示
println!("{}", count);
}【実行結果】 5
sum / product:数値の合計や積を計算する
sum は要素の合計、product は要素の積を計算するための消費アダプタです。これらは、数値を集約したいときに便利です。
fn main() {
let numbers = [1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().sum();
let product: i32 = numbers.iter().product();
println!("sum: {}", sum);
println!("product: {}", product);
}【実行結果】 sum: 15 product: 120
sum や product は、戻り値の型をコンパイラが決められないため、基本的には型注釈が必要になります(例「sum : i32」)。ただし、関数の戻り値型などから型が明らかな場合は、型推論により型注釈を省略できることもあります。なお、sum は数値型に対してのみ使用可能です。
max / min:最大値や最小値を取得する
max と min は、それぞれ最大値・最小値を取得するための消費アダプタです。
fn main() {
let numbers = [3, 2, 5, 1, 4];
// 最大値と最小値を求める
let max = numbers.iter().max();
let min = numbers.iter().min();
// 結果を表示
println!("max: {:?}", max);
println!("min: {:?}", min);
}【実行結果】 max: Some(5) min: Some(1)
max と min については、戻り値が Option 型になる点に注意してください。これは、イテレータが空の場合は、最大値や最小値を返せないためです。
fold / rfold:畳み込み計算を行う
fold や rfold は、イテレータの要素を順番に取り出しながら 1 つの値にまとめるための消費アダプタです。fold は前から、rfold は後ろから計算する点に違いがあります。
基本的な構文は以下のような形となります。
iterator.fold(初期値, |累積値, 要素| 処理)
- 初期値:最初に累積する値
- 累積値:これまでの計算結果
- 要素:イテレータから取り出した要素
以降では、簡単な合計の例で fold と rfold の使い方を見てみましょう。なお、合計を例にするだけで、fold / rfold では任意の処理ができる点が非常に強力です。
fold(前から畳み込む)
fold は、イテレータの要素を前から順に処理しながら、1 つにまとめます。
合計 (sum) を fold で計算する例
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// fold を使用して、イテレータの要素の合計を求める
let sum = numbers.iter().fold(0, |acc, x| acc + x);
// 結果を表示
println!("sum: {}", sum);
}【実行結果】 sum: 15
例では、「初期値: 0」「0 + 1 = 1」「1 + 2 = 3」…「10 + 5 = 15」のように計算が繰り返し進んで 1 つの合計結果にまとまります。この例から、分かるように上記で紹介した sum や product などは、fold でも実現することができます。
任意の処理を定義可能
上記では分かりやすいように合計の例を紹介しましたが、fold は任意の処理を定義できます。以下は、文字列を連結する例です。
fn main() {
let words = vec!["Hello", " ", "Rust", "!"];
// fold を使用して、イテレータの要素を連結する
let concatenated = words.iter().fold(String::new(), |acc, word| acc + word);
// 結果を表示
println!("{}", concatenated);
}【実行結果】 Hello Rust!
このように、fold は文字列の結合などにも使えます。fold や後述する rfold は要素をどのように 1 つの値へまとめるかを柔軟に定義できる点が大きな特徴です。
rfold(後ろから畳み込む)
rfold は、fold と同じように値を 1 つにまとめますが、後ろから順に処理する点が異なります。数値計算だと計算順序が分かりにくいため、文字列結合で比較してみましょう。
fn main() {
let words = vec!["a", "b", "c", "d"];
// fold を使用して、前から順に要素を結合する
let forward = words.iter().fold(String::new(), |acc, word| acc + word);
// rfold を使用して、逆順に要素を結合する
let backward = words.iter().rfold(String::new(), |acc, word| acc + word);
// 結果を表示
println!("forward: {}", forward);
println!("backward: {}", backward);
}【実行結果】 fold: abcd rfold: dcba
例のように、fold では "abcd" のような順になりますが、rfold では、"dcba" のように処理の順序が逆になっています。数値計算などでも、処理の順序が変わる点は同様です。
その他の便利な消費アダプタ
その他の消費アダプタ一覧
上記で紹介した消費アダプタの他にも多くの消費アダプタがありますが、ここでは、代表的なものを一覧で簡単に紹介します。
| 消費アダプタ | 概要 |
|---|---|
any / all | 条件を満たす要素の有無を判定する。 |
position / rposition | 条件の要素を満たす要素の位置を返す。 |
nth / nth_back | n 番目の要素を取得する。nth(0) は next() と同じ。 |
last | 最後の要素を取得する。 |
find / rfind / find_map | 条件に一致する要素を検索する。 |
partition | 条件に応じて 2 つに分割する。 |
for_each / try_for_each | 各要素に処理を適用する。 |
まとめ
Rust の イテレータ(Iterator)に消費アダプタの基本について解説しました。
イテレータは next を使って順番に値を取り出す仕組みであり、その要素を最後まで、または途中まで使うことを「消費」と言います。消費アダプタは、そのイテレータを消費しながら最終的な結果を得るためのメソッドです。
この記事では、代表的な消費アダプタとして、次のようなものを紹介しました。
collect:コレクションにまとめるcount:件数を取得するsum/product:合計や積を計算するmax/min:最大値や最小値を取得するfold/rfold:任意の処理で 1 つの値にまとめる
まずは collect、count、sum などの基本的な消費アダプタに慣れてもらい、そのうえで fold を理解すると、イテレータの仕組みをより深く理解できるようになります。







