Rustコトハジメ

プログラミング言語Rustに関する情報をお届けします。

try!マクロはfromを使ってエラーをリターンする

この記事では、少しoutdatedな話題ではあるようなのですが、以下のドキュメントから、Rustを書くに当たって知るべきエラーハンドリングについて基礎を学んでみたいと思います。

https://doc.rust-jp.rs/the-rust-programming-language-ja/1.6/book/error-handling.html

try!マクロの概要

おそらく他言語の人がRustを使いたくなる理由の一つに、try!マクロの存在があるのではないかと思います。

プログラミングの技法の一つとして「早期リターン」があります。これは、エラーが起きた場合にさっさとリターンしてしまうことで、正常パスがより見えやすくなるという技法です。

Rustでは、エラーが起こる関数はResult型を返します。これは、静的に型チェックをするためですが、try!マクロは、try!(f())

match f() {
    Ok(x) => x,
    Err(e) => return e
}

のように展開してくれるマクロです。こうすることで、

fn myfunc() -> Result<i32, Error> {
    let a = try!(f());
    let b = try!(g(a))
    Ok(b)
}

のように書いていくことが出来ます。もし、fやgの実行でエラーが起きた場合、早期リターンします。

なぜtry!マクロが必要なのか

例えば、ファイルから文字を読み込んでから、これをパースするようなコードを考えます。これらのエラーは型が違います。IOErrorとParseErrorということにします。

unwrapする

この時、簡単なアプリケーションならば、すべてunwrapするのも許されるかも知れませんが、もう少しまともにエラーを返したいと思うことにします。

コンビネータの限界

次の案は、and_thenコンビネータを使うことです。しかしこれはand_thenの型が

fn and_then<U, F>(self, op: F) -> Result<U, E>

なので、エラーの型を変えることが出来ません。これはScalaのflatMapでも同じです。

def flatMap[A1 >: A, B1](f: (B) ⇒ Either[A1, B1]): Either[A1, B1]

Rustは静的ディスパッチを採用しているので、EはIOエラーもパースエラーの最大公約数的に文字列でエラーを表現することになります。and_thenの中で毎回map_err(|e| e.to_string())して文字列に丸めていくのも鬱陶しいですが、何より、型の情報が欠落します。だから、エラーに対して型によってパターンマッチをすることが出来なくなります。

独自のエラー型を定義する

型が欠落するのが嫌ならば、

enum MyError {
    IO(IOError),
    Parse(ParseError),
}

のように独自のエラー型を定義してしまうのがいいと思いますが、ライブラリを作るならこうするのも良いといえますが、アプリケーションではやりすぎと言えます。

try!Box<std::error::Error>を返す

ここで、try!が登場します。

try!を使うと、

  • 書きやすさ: コンビネータの煩雑さがなくなる
  • エラー情報: Box<std::error::Error>(すべてのエラーの親クラス)を返すことにすれば、
    • アプリ向け => 具体的な型情報は欠落してしまうがStringよりはマシな情報が得られる
    • ライブラリ向け => 独自型を定義した場合もtry!で簡潔に書くメリットは残る

という利点があります。

try!がどうやって動作するか

fn parse(s: &str) -> Result<i32, std::io::Error> {
    try!(s.parse::<i32>());
}
fn main() {
    let n = parse("10");
    println!("{}", n);
}
error[E0277]: the trait bound `std::io::Error: std::convert::From<std::num::ParseIntError>` is not satisfied
 --> prog.rs:2:5
  |
2 |     try!(s.parse::<i32>());
  |     ^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::convert::From<std::num::ParseIntError>` is not implemented for `std::io::Error`

この意味のないふざけたコードによってエラーを出してみると、なぜかtry!の中でFromの実装が求められていることがわかります。

実は、try!は、

macro_rules! try {
    ($e:expr) => (match $e {
        Ok(val) => val,
        Err(err) => return Err(From::from(err)),
    });
}

のような定義になっています。

冒頭で紹介したmyfuncを思い出してみます。ここで、fはIOErrorを返しますし、gはParseErrorを返します。

fn myfunc() -> Result<i32, Box<Error>> {
    let a = try!(f());
    let b = try!(g(a))
    Ok(b)
}

では一体これがどうやってBox<Error>に変換されているでしょうか。それこそがまさに「Fromを実装してください」なのです。

impl<'a, E: Error + 'a> From<E> for Box<dyn Error + 'a> {

最新のエラーハンドリング

今現在では、この初期の方法は見直されて、新しいエラーハンドリングの方式が推奨されているようです。別の機会に調査することにします。

qiita.com