ハイサイ!オースティンやいびーん
概要
RustのVector(配列)を別の型に畳む時に使うstd::iter::Iterator.fold
とstd::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
があまり合いません。
reduce
はfold
と違って、引数に初期値を渡せません。その代わり、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
のコールバックに入ってくるaccumulator
とpart
は、String
のreferenceでしかないのです。つまり、文字列をaccumulator
に足していけないのです。
なので、この問題を解消するにはいくつか手があります:
-
.map
を使って、&String
の要素をString
に変換しておく - 「やっぱり
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
がこの問題にあっていますね。
しかし、上記のような問題で分かっていただいたように、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
どちらもうまくいきました!
まとめ
fold
とreduce
の使い方を紹介してきましたが、いかがでしょうか?
解決しようとしていた問題に対して、本記事の命が危なくなったreduce
の不適切さが浮き彫りになりましたが、ある意味それも参考になったらと思います。適している問題とそうでない問題があります。
プログラミングでは常に解決の道が百本あるのですが、そのもっとも近距離の道は一つしかありません。その道を見極められるかどうかは我々職人の技量にかかりますが、筆者はRustの勉強を通して自分の未熟さを改めて実感しました。
Rustは難しい。
しかし、メモリについて考えさせられるから、パフォーマンスについても真剣に考えるし、より良いプログラマーになる訓練になるかなと願って思っています!
筆者と同様にRustを勉強している方、まじゅん ちばらな!