🧱

〚Rust〛thiserrorでトレイトの返すべきエラーを考える

に公開

#[source]とトレイト拡張が便利という話。

背景

thiserrorで様々なライブラリから出るエラーを丁寧に扱うと、以下のようなコードになる。

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("serde json error: {0}")]
    SerdeJson(#[from] serde_json::Error),
    // などなど
}

単一モジュールであればこれで良いが、拡張可能性を持たせようとすると問題が生じる。

例えば抽象化されたデータストアを扱うことを考える。

struct DataSet {
    // 何らかのフィールド達
}

trait Repository {
    fn get(&self) -> Result<DataSet, RepositoryError>;
    fn update(&mut self, dataset: DataSet) -> Result<(), RepositoryError>;
}

このRepositoryErrorが問題で、最初に書いたErrorのような定義だとRepositoryトレイトの実装ごとに必要な外部ライブラリ(例えばMySQL、S3など)を全てRepositoryErrorに入れておく必要がある。これだとせっかく抽象化したにもかかわらず、結局は抽象が実装に依存してしまうことになる。つまり実装者はこのライブラリを使いたいのでRepositoryErrorの定義に追加して欲しいんですけど、と抽象の側に一々言わなければならない。

#[source]

ここでstd::error::Errorを見るとsource()Option<&(dyn Error + 'static)>を返すようになっていて、任意の内なるエラーを返せるようになっている。要するに実装側の様々なエラーはこのsource()で返るようにしておけば良いのではないか。

…というのをちゃんとthiserrorで実現できて、#[source]を使えば良い。

use std::error::Error;

#[derive(thiserror::Error, Debug)]
#[error("{component} error: {source}")]
pub struct RepositoryError {
    component: String,
    #[source]
    source: Box<dyn Error + Send + Sync + 'static>,
}

例えばMySQLを使用する実装ならcomponent"mysql"を入れるなどして、発生源が分かるようにする。

トレイト拡張

このErrorを使う場合、一々some_func().map_err(|e| Error { component: String::From("mysql"), source: e })などと書くのは面倒すぎる。some_func().into()のように簡潔に書けるのが理想だ。

ただimpl<T: std::error::Error + Send + Sync + 'static> Into<Error> for Tのようなものを実装側で書こうとすると、TIntoも自分で定義している型ではないのでいわゆる孤児ルールに引っ掛かる。

そこでトレイト拡張により、例えばsome_func().wrap()と書けるようにする。(このWrapExtは実装ごとに定義する想定でcomponentを固定している。)

use std::error::Error;

pub trait WrapExt<T> {
    fn wrap(self) -> Result<T, RepositoryError>;
}

impl<T, E> WrapExt<T> for Result<T, E>
where
    E: Error + Send + Sync + 'static,
{
    fn wrap(self) -> Result<T, RepositoryError> {
        self.map_err(|e| RepositoryError {
            component: String::from("mysql"),
            source: Box::new(e),
        })
    }
}

すると次のように、かなり記述が簡単になるだろう。

impl Repository for MySqlRepository {
    fn get(&self) -> Result<DataSet, RepositoryError> {
        let row = self.query_row().wrap()?;
        // ...
        Ok(row)
    }

    fn update(&mut self, dataset: DataSet) -> Result<(), RepositoryError> {
        self.exec_update(dataset).wrap()
    }
}

結び

これで抽象側は実装に全く関知せず、実装側も自由に書けるようになった。依存性逆転の原則ここにあり。

Discussion