Help us understand the problem. What is going on with this article?

Rustのエラー処理(1)

RustBookのエラー処理のページが個人的にわかりにくかったのでかみ砕いたものになります。とくに目新しい情報はありません。RustBookのセカンドエディションを順番に読んでいけば多分困りませんが、つまみ食いして理解するタイプの人はぜひ参照してみてください。

元が長いせいでこっちもめっちゃ長くなります。なのでシリーズものにしました。

安全性のためにプロセスを終了させる

 ソフトウェアの安全性を保つためにも、エラー処理は欠かせません。以下のコードを見てください。

// 1から10までの数字を想像(この場合は5)し、
// それと一致していればtrue、そうでなければfalseを返します
fn guess(n: i32) -> bool {
    if n < 1 || n > 10 {
        panic!("Invalid number: {}", n);
    }
    n == 5
}

fn main() {
    guess(11);
}

このコードは1から10の間にない数字なので、panic!によって処理が終了(パニック)します。

use std::env;

fn main() {
    let mut argv = env::args();
    let arg: String = argv.nth(1).unwrap(); // 引数がなかったら落ちる
    let n: i32 = arg.parse().unwrap(); // 引数が数字じゃなかったら落ちる
    println!("{}", 2 * n);
}

ko
このコードも落ちます。Rustにおけるunrwapは、「なにかあったらとにかくアプリを終了させろ」と同義になります。ソフトウェアは安全になりますが、何かあるたびにソフトが落ちるので、とても使いづらくなりますね。

unwrapとは

 直前のコードにはpanic!()を呼んでいないのにパニックしました。unwrapにpanicの呼び出しの処理が処理が組み込まれています。
このunwrapについてより理解を深めるために、Option型とResult型について調べていきます。

Option型について

Optionha以下のように定義されます

enum Option<T> {
    None,
    Some(T),
}

Option型によって値を返却することで、プログラマーにNoneの時の処理を強制することができます。以下の例では、文字列から特定の文字を探す関数です。

// haystack文字列から needleの文字を探します。 もし見つかったら
// 文字のバイトオフセットを返します(ポインタの距離みたいなものです)。なければNoneを返します。
fn find(haystack: &str, needle: char) -> Option<usize> {
    for (offset, c) in haystack.char_indices() {
        if c == needle {
            return Some(offset);
        }
    }
    None
}

この時、offsetではなく、Option型を返しています。
Someはヴァリアント(変数みたいなもの)ですが、fn(value: T) -> Option型の関数と考えることができます。Noneは引数をもたない関数と考えることができます。

そして以下がそれによってファイル名の拡張子を出力するプログラムです。

fn main() {
    let file_name = "foobar.rs";
    match find(file_name, '.') {
        None => println!("No file extension found."),
        Some(i) => println!("File extension: {}", &file_name[i+1..]),
    }
}

このコードを見ると、パターンマッチングの使用を強制していて、None時の処理を義務付けることができることがわかります。

しかし、unwrapを使ってるときはどうなっているのでしょうか?

enum Option<T> {
    None,
    Some(T),
}

impl<T> Option<T> {
    fn unwrap(self) -> T {
        match self {
            Option::Some(val) => val,
            Option::None =>
              panic!("called `Option::unwrap()` on a `None` value"),
        }
    }
}

 つまり、一度Optionに包んだものからまた値を取り出して返り値にしています。なので、unwrap()を使うと、panic!とは両立できません。というか意味ありません。

Option値の組合せ

 ファイル名の拡張子を得る関数は以下のようにして作ります(*業務プログラミングでは標準ライブラリを使用してください)

// find関数は省略(実行するならコピペしてね)
// "."から始める拡張子が定義されていれば、
// その名前を返します
// "."がなければNoneを返します
fn extension_explicit(file_name: &str) -> Option<&str> {
    match find(file_name, '.') {
        None => None,
        Some(i) => Some(&file_name[i+1..]),
    }
}


// mainはこんな感じ
fn main() {
    let file_name = "foobar.rs";
    match extension_explicit(file_name) {
        None => println!("No file extension"),
        Some(extension_name) => println!("File extension: {}", extension_name),
    }
}

 こうすることで、拡張子名を取得することができますが、None vs Some(T)みたいなパターンマッチングは何回もやるので、もう少し簡潔に書きたいという需要があるかもしれません。
 Rustはポリモーフィズムを採用しているので、このパターンを抽象化するコンビネーターは簡単に定義できます。

fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A {
    match option {
        None => None,
        Some(value) => Some(f(value)),
    }
}

// 以下のように書き直せる

fn extension(file_name: &str) -> Option<&str> {
    find(file_name, '.').map(|i| &file_name[i+1..])
}

 このあたりはクロージャやトレイト境界の話が関わってくるので省略します。
 この説明は難しいかもしれませんが、Noneにデフォルト値を与えてしまうやり方はかなりわかりやすいと思います。

fn unwrap_or<T>(option: Option<T>, default: T) -> T {
    match option {
        None => default,
        Some(value) => value,
    }
}

この時、OptionのTの型とdefalt値が一致していないといけません。main関数を

fn main() {
    let file_name = "foobar";
    println!("File extension: {}", extension(file_name).unwrap_or("None"));
}

にすると、File extension: None と出てきます。

※unwrap()もunrap_or()もunwrap_or_else()もOption型のメソッドとして定義されているので、それを使うようにしましょう。

and_thenコンビネータについても説明しましょう。「..」などはファイル名の拡張子が「.」と判定されてしまいます。なので、次の明示的な場合分けをやる必要があります。

fn file_path_ext_explicit(file_path: &str) -> Option<&str> {
    match file_name(file_path) {
        None => None,
        Some(name) => match extension(name) {
            None => None,
            Some(ext) => Some(ext),
        }
    }
}

fn file_name(file_path: &str) -> Option<&str> {
  // 「./user/hogehoge/a.txt」みたいな絶対パスからファイル名「a.txt」を取り出す実装
  unimplemented!()
}

この例ではmapを使うのは難しいでしょう。mapの引数に渡した関数は中の値にだけ適用され、必ずSomeに包まれて返されます。SomeではなくOptionを返せる仕組みが必要で、and_thenを使うとシンプルになります。and_thenは以下のような実装になっています。

fn and_then<F, T, A>(option: Option<T>, f: F) -> Option<A>
        where F: FnOnce(T) -> Option<A> {
    match option {
        None => None,
        Some(value) => f(value),
    }
}
fn file_path_ext(file_path: &str) -> Option<&str> {
    file_name(file_path).and_then(extension)
}

このようにして明示的な場合分けを減らしてくれます。

用語説明

ヴァリアントについて

 tuple variantsと呼ばれたりします。列挙子が多分正しい表現ですが、口でenumを説明するときに、「変数どれかとるやつ」みたいに説明するので変数みたいなやつと説明しました。いい表現誰か教えてくれ

パラメトリックポリモーフィズム

 多相性とかいろいろ言われますね。ザックリ言えば、同じ名前の関数に違う型の値入れたらそれに応じていい感じにやってくれる関数とかです。

コンビネーター

Yコンビネータについて調べてみたを参照してください。正直分かってない。Haskellやるしかないのか……

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした