LoginSignup
3
0

Rust: .foldと.reduceの違いと注意点

Last updated at Posted at 2023-08-16

ハイサイ!オースティンやいびーん

概要

RustのVector(配列)を別の型に畳む時に使うstd::iter::Iterator.foldstd::iter::Iterator.reduceの使い方と注意点を紹介します。

なぜfoldとreduceを使うのか

foldとreduceは配列に入っている要素を一つの値に変換する時にとても強力なツールになります。

パフォーマンスの判断で場合によっては相応しくないのですが、困ったら基本的にこの二つのメソッドを使えば配列の操作ならなんでも可能です。

問題紹介

本記事の概要を説明するために、まず設問します。

Vectorに入った人の名前の文字列があります。様々な国々の名前も出てきます。Sri Lankaの方のように名前がとても長いこともあります。

そのVectorを英語の敬称付きの単一の文字列Stringに変換する関数を書きたいのです。

性別も引数として受けて敬称を変えます。

enum Sex {
    Male,
    Female,
}

fn get_name_with_title(name_parts: &Vec<String>, sex: &Sex) -> String {
  // implementation
}

foldを使った場合

foldを使った場合、まず、初期値が必要です。まず、性別の敬称を作るロジックを追加しましょう。

fn get_title_by_sex(sex: &Sex) -> &'static str {
    match sex {
        Sex::Male => "Mr.",
        Sex::Female => "Ms.",
    }
}

戻り値の型を&'static strにすることによってこれらの敬称のstrがランタイムのコードにコンパイルされるらしいのです!

この関数を使って初期値を生成しましょう。

fn get_name_with_title(name_parts: &Vec<String>, sex: Sex) -> String {
    let title: String = get_title_by_sex(sex).to_string();
    //
}

この初期値を使ってfoldをしたいのですが、まずVectorのIteratorを取得しないといけないのです。以下のようにname_partsにはiterというメソッドがあります:

name_parts.iter()

このメソッドがIteratorというものを返してくれて、それのメソッドにfoldが入っています。

foldを使ってみましょう。

    name_parts
        .iter()
        .fold(title, |accumulator, part| accumulator + " " + part)

foldは一番目の引数に初期値をもらいます。それがコールバックの最初のaccumulatorの値になります。コールバックではaccumulatorに文字列を足していきます。

初期値を渡しているので、例えVectorの配列が空でもStringを渡してくれます ので時に注意することはないです。

全体で見るとこのように完成します。

fn get_name_with_title(name_parts: &Vec<String>, sex: &Sex) -> String {
    let title = get_title_by_sex(sex).to_string();

    name_parts
        .iter()
        .fold(title, |accumulator, part| accumulator + " " + part)
}

.reduceで同じようなことをしようとしたら、

.reduceを使って同様な実装を試してみましょう。あらかじめ言っておきますが、今回の問題には.reduceがあまり合いません。

reducefoldと違って、引数に初期値を渡せません。その代わり、reduceはVectorの最初に値をaccumulatorにして処理をしてくれます。

前回と同様に単に.foldを変えてみましょう。


fn get_name_with_title_reduce(name_parts: &Vec<String>, sex: &Sex) -> String {
    let title = get_title_by_sex(sex).to_string();

    let full_name = name_parts
        .iter()
        .reduce(|accumulator, part| accumulator + " " + part);

    format!("{title} {full_name}")
}

このコードにはRustのコンパイラーを怒らせる箇所が二つありますが、読者はお分かりでしょうか。

初期値がないかもしれない問題

まず、reduceの結果が一つ大きな問題になっています。先ほど説明した通り、初期値はなく、配列の最初の値から始めます。では、空の配列だったら、reduceの結果はどうなりますか?

答えは、何も出てこない可能がある、つまりRustのnullならずNoneが帰ります:

let full_name: Option<String> = name_parts
        .iter()
        .reduce(|accumulator, part| accumulator + " " + part);

なので、Noneの可能性に対してなんらかの処理をしないといけません。

let full_name: Option<String> = name_parts
        .iter()
        .reduce(|accumulator, part| accumulator + " " + part);
let full_name = match full_name {
        Some(s) => s,
        None => String::new()
    };

もしくはもっと簡単に

let full_name = full_name.unwrap_or_default();

引数がReferenceという問題

まだ重大なエラーが潜んでいるのです。実は、reduceのコールバックに入ってくるaccumulatorpartは、Stringのreferenceでしかないのです。つまり、文字列をaccumulatorに足していけないのです。

なので、この問題を解消するにはいくつか手があります:

  1. .mapを使って、&Stringの要素をStringに変換しておく
  2. 「やっぱりfoldがあっていよね」と諦める

2が正しいのですが、それだとreduceを紹介できないので、このとても役立つstackoverflowの知見を拝借して以下のように解決します。

fn get_name_with_title_reduce(name_parts: &Vec<String>, sex: &Sex) -> String {
    let title = get_title_by_sex(sex).to_string();

    let full_name = name_parts
        .iter()
        .map(|part| part.to_string())
        .reduce(|accumulator, part| accumulator + " " + &part);
    let full_name = full_name.unwrap_or_default();

    format!("{title} {full_name}")
}

コード量も含めて、明らかにfoldがこの問題にあっていますね:sweat_smile:

しかし、上記のような問題で分かっていただいたように、reduce には落とし穴があるのです!

関数を使ってみる

両方とも実際に使ってみましょう。

fn main() {
    let name_vec = vec![
        "Austin".to_string(),
        "John".to_string(),
        "Mayer".to_string(),
    ];
    let s = Sex::Male;

    let name = get_name_with_title(&name_vec, &s);
    println!("Name: {}", name);

    let name = get_name_with_title_reduce(&name_vec, &s);
    println!("Name: {}", name);
}

STDOUT:

Name: Mr. Austin John Mayer
Name: Mr. Austin John Mayer

どちらもうまくいきました!

まとめ

foldreduceの使い方を紹介してきましたが、いかがでしょうか?

解決しようとしていた問題に対して、本記事の命が危なくなったreduceの不適切さが浮き彫りになりましたが、ある意味それも参考になったらと思います。適している問題とそうでない問題があります

プログラミングでは常に解決の道が百本あるのですが、そのもっとも近距離の道は一つしかありません。その道を見極められるかどうかは我々職人の技量にかかりますが、筆者はRustの勉強を通して自分の未熟さを改めて実感しました。

Rustは難しい

しかし、メモリについて考えさせられるから、パフォーマンスについても真剣に考えるし、より良いプログラマーになる訓練になるかなと願って思っています!

筆者と同様にRustを勉強している方、まじゅん ちばらな!

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0