Chapter 12

エラー処理

📌 panic!

基本的に復帰不能なエラーが発生したら,どうしようも出来ません.メッセージを表示してプログラムを終了する手っ取り早い方法が panic! です

fn main() {
    panic!("crash");
}

📌 Result 型

どこかでエラーが発生したとしても,すぐにプログラムを終了させるわけにはなかなかいきません.ある関数の内部でエラーが発生したら,それを呼び出し元に知らせる必要があります.そこで Result 型が使われます.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

ここではファイル処理を考えてみます.既存のファイルを開いて処理をしたいとします.もし,ファイルが無ければ作成します.その場合,最初にファイルを開こうとしたときにエラーが発生し,そのエラーがファイルが無かったことを表していればファイルを新規に作成するようにします.ファイルを開く処理 File::openstd::io::Result 型を返します.これは Result<T, Error> 型の別名です.

use std::{fs::File, io::ErrorKind};

fn main() {
    let f = File::open("hello.txt");
    let f = match f {
        Ok(file) => file,
        Err(ref error) if error.kind() == ErrorKind::NotFound => {
            match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
            }
        },
        Err(error) => {
            panic!("There was a problem opening the file: {:?}", error)
        },
    };
}

📌 unwrap, expect

Option 型, Result 型ともに,値を取り出す unwrap という関数があります.これは,もし値が NoneErr のときに, panic! を呼び出します.

pub fn unwrap(self) -> T

unwrappanic! を呼び出すと,標準のエラーメッセージが表示されますが, unwrap の代わりに expect を呼ぶと,エラーメッセージに情報を追加することが出来ます.

pub fn expect(self, msg: &str) -> T

Option 型と Result 型の unwrap は以下のように match 式を短くしたものです.

option.unwrap()
//
// ↓
//
match option {
    Some(v) => v,
    None => panic!(...),
}
result.unwrap()
//
// ↓
//
match result {
    Ok(v) => v,
    Err(e) => panic!(...),
}

📌 エラー伝搬

Option 型, Result 型を返す関数の中で,値が None または Err のときに,処理を中断して呼び出し元に値を返す仕組みが用意されています.それは ? 演算子を使います.

fn hoge() -> Option<i32> {
    let a = Some(10);
    let b = a?;
    Some(b)
}
fn hoge() -> Option<i32> {
    let a = None;
    let b = a?; // return Option<i32>::None
    Some(b)
}

📌 コンビネータ

Option 型, Result 型も コンビネータ です.このコンビネータがエラー処理のコードを大幅に削減してくれます.手続き型であれば,1つ1つの関数呼出しの結果がエラーか無効な値かを確認していきます.これだと,確認コードが大量に出来てしまいます.そこで,まずはエラー伝搬です.関数が OptionResult を返せば, ? 演算子を使ってチェーン方式で処理を記述することが出来ます.途中でエラーが発生すれば,処理を打ち切ってエラー伝搬されます.

let ret = open()?.read()?.replace()?.write()?.close()?;

コンビネータとは簡単に言うと, 高階関数 のことで,高階関数とは関数を引数に取る関数のことです.例えば,関数 f(x)g(x),これらの合成関数が (f\circ g)(x) = f(g(x)) とします.この場合,この \circ がコンビネータで,2つの関数を取っています.先程の open.read.replace.write.close で考えてみると close(write(replace(read(open())))) の関係に見えないでしょうか.ここで . 演算子がコンビネータであり,その役を担っているのが Option 型と Result 型と考えることが出来ます.

Option 型, Result 型にはコンビネータとしての便利なメソッドが多く用意されています.基本的なものとして, map は値に関数を適用して,その結果をコンビネータに変換します.

pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U>     // Option
pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Result<U, E> // Result

and_then は関数を適用して,その結果をそのまま返します.つまり, and_then に渡す関数はコンビネータを返します.

pub fn and_then<U, F: FnOnce(T) -> Option<U>>(self, f: F) -> Option<U>        // Option
pub fn and_then<U, F: FnOnce(T) -> Result<U, E>>(self, op: F) -> Result<U, E> // Result

コンビネータは型を合わせる必要があります. Option のメソッドに渡す関数は.単純に T 型を返す関数や Option を返す関数ならよいのですが, Result を返す場合にはそのままでは利用できません.そこで, OptionResult には相互に変換するメソッドがいくつかあります.例えば, ok_orOption から Result に, okResult から Option に変換します.これにより OptionResult を返す関数を1つのメソッドチェーン内に利用することが出来ます.

コンビネータは ? 演算子を使っていなければ,途中の処理で None になったり, Err になった場合,チェーンの最後の型で返ってきます.これによりエラー処理を書く場所が少なくなります.

構造体のメソッドのところでも少し触れましたが,コンビネータのメソッドの引数は self が多いです.これは,メソッド呼び出しで, Option<u32>Option<f32> になったり, Option<T>Result<T,E> になったり型の変換を行っているからです.