LoginSignup
11
3

More than 1 year has passed since last update.

Rustで「あ、やっぱいいです」したい

Last updated at Posted at 2020-05-21

あまりいい例が思いつかないのですが、「あわよくば」実行したい処理というのがプログラミングにはよく出てきたりします。

特に今回は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文が追加されました!

本記事で最も求めていたのはまさしくこの構文です。

Rust
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を返すべきでは?」と考えるようになったので少し複雑な気持ちです。

詳しくは記事を書いたのでこちらを見ていただけると幸いです。

11
3
8

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
11
3