あまりいい例が思いつかないのですが、「あわよくば」実行したい処理というのがプログラミングにはよく出てきたりします。
特に今回はRustの Option
型の話をします。以下「辞書に載っているかを見て、 Some
なら情報を出力、 None
なら何もしない」という関数を考えてみます。なかったら「あ、やっぱり何もしなくていいです」ということで処理を打ち切り、関数を抜けたいわけです。
use std::collections::HashMap;
struct Person {
name: String,
age: usize,
}
fn search(book: &HashMap<usize, Person>, id: usize) {
// 辞書からidに登録されたPersonを取ってきて、あったら以下を実行
println!("[HIT] name: {}, age: {}", p.name, p.age);
}
fn main() {
let mut book = HashMap::new();
let p = Person {
name: "namnium".to_string(),
age: 21,
};
book.insert(100, p);
search(&book, 100); // 出力あり
search(&book, 200); // 出力なし
}
unwrap
を使う
他の言語とかだと「バリデーション」というシーンで同様のことを行うことが多いです。
NULL
チェックと同じノリで、こうやって書きたくなります。
fn search(book: &HashMap<usize, Person>, id: usize) {
let p_wrapped = book.get(&id);
if p_wrapped.is_none() { return; }
let p = p_wrapped.unwrap();
println!("[HIT] name: {}, age: {}", p.name, p.age);
}
is_none
メソッドでバリデーションし、ダメなら抜ける、大丈夫なら unwrap
という感じです。
「ダメだったらリターン」はどの言語でも「メインの処理が条件分岐の枝には入ってこない」ので、インデントを深くしたくない場合おすすめの書き方です。
しかし unwrap
を使うのは正直あまり美しくないです...
2021/4/24 追記
この方向性で行く場合、anyhow
クレートのensure!
マクロを使い、さらに冗長にするといい感じかもしれません。一つの選択肢として追加しておきます。(シグネチャは大きく変わっています。)
ただ表題は解決していない感じなのと、やっぱり今回はunwrap
を使うことになるのがなんとも言えないです。このシーンでResult
型を返す場合は、後述の?
のほうが賢いです。
fn search(book: &HashMap<usize, Person>, id: usize) -> Result<String> {
let p_wrapped = book.get(&id);
ensure!(p_wrapped.is_some(), "book doesn't have id:{} item.", id);
let p = p_wrapped.unwrap();
let res = format!("[HIT] name: {}, age: {}", p.name, p.age);
Ok(res)
}
if let
を使う
Rustらしい書き方として if let
式の使用が考えられます。
fn search(book: &HashMap<usize, Person>, id: usize) {
if let Some(p) = book.get(&id) {
println!("[HIT] name: {}, age: {}", p.name, p.age);
}
}
パターンマッチングによるアンラップはとてもRustらしいでしょう。
しかし、メインの処理にも関わらずインデントが深くなってしまうのがいただけないです。今回みたいな例ならいいですが、メインの処理が10行以上になってくるとこの方法はまずいです。
match
を使う
Rustらしくパターンマッチを活かしつつ、インデントも保ちたいとなると、 match
式か if let ... else
式を使用し unwrap
の代用とする案が考えられます。今回は match
を使用します。
fn search(book: &HashMap<usize, Person>, id: usize) {
let p = match book.get(&id) {
Some(v) => v,
None => return,
};
println!("[HIT] name: {}, age: {}", p.name, p.age);
}
しかし None => return
というわかりきった処理のためにこれだけ行を消費するのはなんとも言えない感覚になります。
?
を使う
ここまで記事を読んで「try!マクロをご存知ない?」みたいに思った方もいるかもしれません。そうです、Rustのエラーハンドリング手法を使用することでも解決できます。
fn search(book: &HashMap<usize, Person>, id: usize) -> Option<()> {
let p = book.get(&id)?;
println!("[HIT] name: {}, age: {}", p.name, p.age);
Some(())
}
しかし関数のシグネチャが変わっていますし、そもそも今回は「エラーが起きるかもしれない関数」の話ではなく「あわよくば実行して欲しい処理をもった関数」の話をしているわけですから、一見きれいに見えますがあまり取りたい手段ではないです。
2021/4/23 追記
?
を使うならばいっそのことResult
型を返したほうがよさそうな気もします。(ensure!
マクロの場合についても考慮すると。)
表題は解決していないですし、シグネチャも全く変わっていますが、返り値のエラー処理という形で書きたい人にとってはこれがベストでしょう。
fn search(book: &HashMap<usize, Person>, id: usize) -> Result<String, String> {
let p = book.get(&id)
.ok_or_else(|| format!("book doesn't have id:{} item.", id))?;
let res = format!("[HIT] name: {}, age: {}", p.name, p.age);
Ok(res)
}
やっぱり match
を使うんだけどマクロ用意してみる
match
式を使う方法は、冗長ながらも紹介した中ではバランスの取れた方法だったように思います。これをマクロ化してしまえばすっきりしそうです。
macro_rules! or_return {
( $wrapper:expr, $failed:expr ) => {
match $wrapper {
Some(v) => v,
None => return $failed,
}
};
}
fn search(book: &HashMap<usize, Person>, id: usize) {
let p = or_return!(book.get(&id), ());
println!("[HIT] name: {}, age: {}", p.name, p.age);
}
具体例が思いつかないとはいいつつもしょっちゅう書く処理なのでこのマクロの導入は本気で良さそうに思えてしまいます。
この他に Option
で使えそうなメソッドに unwrap_or_else
というのがあるのですが、渡すのはRubyでいうブロックみたいなものではなく思いっきりクロージャなので return
が実行できないです。
で、ここまで偉そうに解説してきたので一見解説記事に見えますが、「これ以上に良い書き方実はあったりしますか?」という本来teratailとかでやるべき「質問」が本記事の真の目的でした。もしこの他に良い書き方をご存知の方おりましたら、ぜひともコメントお願いいたします()
2022/11/04 追記 let-else
文
Rust 1.65.0にてlet-else
文が追加されました!
本記事で最も求めていたのはまさしくこの構文です。
fn search(book: &HashMap<usize, Person>, id: usize) {
let Some(p) = book.get(&id) else { return };
println!("[HIT] name: {}, age: {}", p.name, p.age);
}
...ただ、現在は「素直にResult
を返すべきでは?」と考えるようになったので少し複雑な気持ちです。
詳しくは記事を書いたのでこちらを見ていただけると幸いです。