Rust入門

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

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

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 という消費アダプタが適用されることで計算が実行されて Vecresult_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 を使用する場合には、注意が必要です。以下のように taketake_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

sumproduct は、戻り値の型をコンパイラが決められないため、基本的には型注釈が必要になります(例「sum : i32」)。ただし、関数の戻り値型などから型が明らかな場合は、型推論により型注釈を省略できることもあります。なお、sum は数値型に対してのみ使用可能です。

max / min:最大値や最小値を取得する

maxmin は、それぞれ最大値・最小値を取得するための消費アダプタです。

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)

maxmin については、戻り値が Option 型になる点に注意してください。これは、イテレータが空の場合は、最大値や最小値を返せないためです。

【補足:比較方法を指定する】

max_bymin_by を使用すると、比較方法を自分で指定することができます。また、max_by_keymin_by_key を使うと、キーを指定して比較することも可能です。

fold / rfold:畳み込み計算を行う

foldrfold は、イテレータの要素を順番に取り出しながら 1 つの値にまとめるための消費アダプタです。fold は前から、rfold は後ろから計算する点に違いがあります。

基本的な構文は以下のような形となります。

iterator.fold(初期値, |累積値, 要素| 処理)
  • 初期値:最初に累積する値
  • 累積値:これまでの計算結果
  • 要素:イテレータから取り出した要素

以降では、簡単な合計の例で foldrfold の使い方を見てみましょう。なお、合計を例にするだけで、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 つの合計結果にまとまります。この例から、分かるように上記で紹介した sumproduct などは、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" のように処理の順序が逆になっています。数値計算などでも、処理の順序が変わる点は同様です。

【補足:エラー処理付きの畳み込み】

try_foldtry_rfold を使用すると途中でエラーが発生した場合に処理を中断することができます。

その他の便利な消費アダプタ

その他の消費アダプタ一覧

上記で紹介した消費アダプタの他にも多くの消費アダプタがありますが、ここでは、代表的なものを一覧で簡単に紹介します。

消費アダプタ概要
any / all条件を満たす要素の有無を判定する。
position / rposition条件の要素を満たす要素の位置を返す。
nth / nth_backn 番目の要素を取得する。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 つの値にまとめる

まずは collectcountsum などの基本的な消費アダプタに慣れてもらい、そのうえで fold を理解すると、イテレータの仕組みをより深く理解できるようになります。

あわせて読みたい
Rust プログラミング入門
Rust プログラミング入門
ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました