Rust: failureを用いたエラー型定義

[前回の記事][previous]でMixinを紹介したけれど、その後エラーハンドリングが全然分からず詰まったからメモ。
結論から言うと[failureのbook][failure-book]を読んだら全て解決した。

## failure
自分の理解の確認も兼ねて、failureの説明を少し。

### `Fail`と`Error`
`failure::Fail`トレイトはfailureの中核をなす存在だ。
`Fail`は`std::error::Error`を置き換えることを狙ったもので、トレイト境界が`Display + Debug`から`Display + Debug + Send + Sync + 'static`にパワーアップしている。このパワーアップによってスレッドを跨いだエラー伝達と、トレイトオブジェクトからのダウンキャストがそれぞれコンパイラによって保証されるようになる。

また、`Fail`を実装したものは自動的に`failure::Error`へ一息に変換可能になる。もちろん`?`で。
`std::error::Error + Send + Sync + 'static`な型へのジェネリック実装があるため、おおよそのエラー型は`Fail`だ。
このため、どんなエラー型が来ようともとりあえず`Error`で早期リターンが可能になる。依存ライブラリが増えるたび`From`を増やす必要はないのだ。

```rust
fn do_anything() -> Result<(), failure::Error> {
    foo::bar()?;
    piyo::piyo(hoge::fuga()?)?;
    Ok(())
}
```

自作のエラー型に`Fail`を実装するのも簡単で、`derive`が用意されている。

```rust
#[derive(Debug, Fail)]
pub enum MyError {
    #[fail(display = "何もしていないのに壊れた")]
    DidNothingButBroken,

    #[fail(display = "猫")]
    CatHanger(#[fail(cause)] CatError),
}
```

`Error`でエラーが渡されてきてもダウンキャストしてハンドルを試みることができる。

```rust
match error.downcast::Piyo() {
    Ok(Piyo::RecoverMe) => Ok(...)
    Err(e) => Err(e)
}
```

別のエラーによってエラーが引き起こされる場合、`cause()`で元となったエラーを取り出せるようにできる。`cause`のチェーンをイテレータで辿ることも可能。

```rust
if let Some(caused) = fail.cause() { ... }
```

### *an Error and ErrorKind pair*パターン
[Patterns & Guidenceの章][failure-book/p&g]ではfailureを実際にプログラムに取り入れる際のパターンがいくつか分かりやすく紹介されている。
今回はその内の一つ、*an Error and ErrorKind pair*を使用する。

failureには、元のエラーをラップして別の説明を与える構造体`Context<D>`が存在する。
`Context<D>`は内部に元の`Error`を持つものの、`Display`の実装ではそのエラーの代わりに、与えられた`D`型の構造体/列挙型を用いる。中身が欲しいときは`cause()`。
用途は分かりやすく、依存先のエラーをラップしてコンテキストに応じた情報を付加できる。

`D`型は文字列に限らず`Display + Send + Sync + 'static`であればいい。
そこで、ここに列挙型を渡すことにする;

```rust
#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)]
pub enum ErrorKind {
    #[fail(display = "Database Access failed")]
    DatabaseAccessFailed,

    #[fail(display = "Given Name has been used")]
    NameHasBeenUsed,

    ...
}
```

これで、`Context<ErrorKind>`はよく分からないエラー型`Error`とコンテキスト依存のエラー型`ErrorKind`の二つの情報を含む型になる。`cause()`で元のエラーが、`get_context()`で`ErrorKind`が取り出せる。強そう。

更に`Context<ErrorKind>`をカプセル化する構造体も定義する。現状`derive`が働かないため手作業で`Fail`を実装する。

```rust
#[derive(Debug)]
pub struct Error {
    inner: Context<ErrorKind>,
}

impl Fail for Error { /* self.innerに委譲するだけ */ }

impl Display for Error { /* self.innerに委譲するだけ */ }

impl MyError {
    pub fn kind(&self) -> MyErrorKind {
        *self.inner.get_context()
    }
}

impl From<ErrorKind> for Error {
    fn from(kind: ErrorKind) -> Error {
        Error {
            inner: Context::new(kind),
        }
    }
}

impl From<Context<ErrorKind>> for Error {
    fn from(ctx: Context<ErrorKind>) -> Error {
        Error { inner: ctx }
    }
}
```

使う時は`failure::Error`の代わりにここで実装した`Error`を用いる。
依存ライブラリ等のエラーはもはや`?`で雑に変換することはできなくなり、エラー型の翻訳作業が必要になる。翻訳といっても分かるエラーを取り分けるだけだから、適度にハンドルが強制されて良さそう。

```rust
use diesel::result::{Error as DieselError, DatabaseErrorKind};

fn create_user(name: String) -> Result<User, Error> {
    // 何かして`Result<T, E>`が来る
    let result = some_command(name);

    let user = result.map_err(|e| match e {
        DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _) => {
            e.context(ErrorKind::NameHasBeenUsed)
        }
        _ => e.context(ErrorKind::DatabaseAccessFailed),
    })?;

    info!("🎉 {}", user);

    Ok(user)
}
```

## サービスでの利用
サービスというからには何かをする。何かをすると、普通、失敗したり成功したりする。サービスのインターフェースと同時にエラーの型についても定義されるべきだろう。
また、サービスのインターフェースからは依存関係を隔離し、何に依存するかは実装次第にしたい。

先述した*an Error and ErrorKind pair*パターンは、こういった層、粒度でのエラーハンドリングに適している。

サービスはドメイン固有の、一般に(実装によらず)発生しうるエラーの種類を`ErrorKind`として定義し、対応する`Error`をインターフェースに含める。
サービスを実装するときは、依存ライブラリや依存サービスのエラーを固有のエラーに翻訳する責任を負う。

より上層の、HTTPリクエストを受けサービスを使って応答するような層では各サービス固有のエラ－を`failure::Error`に、ひいては`500`に変換する。

発生したエラーについて、原因が何で、どのような経路を通ってきたかは知りたいと思う。
エラーはサービスを経由するたびラップされるから、玉ねぎ状に`Error`及び`ErrorKind`の層が重なった状態になる。これを一枚ずつ剥いていけば経路がわかる。

剥いてロギングするための方法は2種類考えられる;

* `error!`するときに`iter_chain()`して`fold()`して`format!()`
* 各サービスの`Error`について`Display`の実装をカスタムし、`cause`を再帰的に表示

どのみちサービスとエラーの種類を同時に出力しようと思うと`Display`の実装を変えることになるため、後者でいいような気がする。

[previous]:https://qiita.com/OvQ/items/679e56435516dcb9eb59
[failure-book]:https://rust-lang-nursery.github.io/failure/intro.html
[failure-book/p&g]:https://rust-lang-nursery.github.io/failure/guidance.html