Chapter 12

エラー処理

mebiusbox
mebiusbox
2023.03.16に更新

📌 panic!マクロ

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

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

📌 Result型

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

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

ここではファイル処理を考えてみます.既存のファイルを開いて処理をしたいとします.もし,ファイルが無ければ作成します.その場合,最初にファイルを開こうとしたときにエラーが発生し,そのエラーがファイルが無かったことを表していればファイルを新規に作成するようにします.ファイルを開くFile::open関数はstd::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

unwrapメソッドがpanic!マクロを呼び出すと,標準のエラーメッセージが表示されますが, 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つの関数呼出しの結果がエラーか無効な値かを確認します.これだと,確認コードが大量にできてしまいます.そこで,まずはエラー伝搬です.関数がOption型かResult型を返せば,?演算子を使ってチェーン方式で処理を記述できます.途中でエラーが発生すれば,処理を打ち切ってエラー伝搬されます.

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型を返す場合にはそのままでは利用できません.そこで,Option型とResult型には相互に変換するメソッドがいくつかあります.例えば,ok_orメソッドはOption型からResult型に, okメソッドはResult型からOption型に変換します.これにより Option型やResult型を返す関数を1つのメソッドチェーン内に利用できます.

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

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