〚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
のようなものを実装側で書こうとすると、T
もInto
も自分で定義している型ではないのでいわゆる孤児ルールに引っ掛かる。
そこでトレイト拡張により、例えば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