前回、 Result<T, E> の E に求められる条件となる std::error::Error トレイトの話をしました。
記事の最後に示唆した通り、 Display 、 Debug 、 std::error::Error を手動実装するのは大変です。
Display 、 Debug 、 std::error::Error トレイトを代わりに実装してくれるのが thiserror::Error deriveマクロ です!ボイラープレート削減!
use thiserror::Error;
#[derive(Error, Debug)] // <- これで impl std::error::Error for DataStoreError となる
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[from] io::Error),
#[error("the data for key `{0}` is not available")]
Redaction(String),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader {
expected: String,
found: String,
},
#[error("unknown data store error")]
Unknown,
}
(公式ドキュメントより引用)
「thiserror!thiserror!」とみんな称えてますが、 thiserror クレートが提供しているのはこのderiveマクロだけです 。ということは! thiserror::Error deriveマクロの使い方・展開結果さえわかれば、thiserrorを理解したということになります。本記事ではこれをまとめたいと思います。
thiserror::Errorの生成物を見てみる
thiserror::Error に存在する不活性属性についての早見表を以下にまとめました。
thiserror 属性早見表
| 属性 | 効果・説明 |
|---|---|
#[error(...)] |
std::fmt::Display の実装時に表示する文言を設定する。 {0} や {var} などでフィールドを埋め込むことができる他、 transparent を指定することで付与対象の表示をそのまま表示する |
#[from] |
属性付与対象フィールドの型からこのエラー型への From トレイトを定義する。また、 #[source] 同様にsourceメソッドでこのフィールドを返すようにする |
#[source] |
sourceメソッドでこのフィールドを返すようにする。 source という名前のフィールドがある場合この属性がついていなくとも同実装が施される |
#[backtrace] |
利用にはnightlyで #![feature(error_generic_member_access)] が必要。付与されたフィールドが持つBacktraceをprovideメソッドを通してこのエラーの provide メソッドへとフォワーディングする。また Backtrace がフィールドに含まれる場合自動的に provide で提供されるようになる |
source メソッドや provide メソッドの解説については公式ドキュメントか前回を参照していただければと思います。
ついでに蛇足かもしれませんが思ったことを...
各属性筆者所感
| 属性 | 所感 |
|---|---|
#[error(...)] |
ほぼ必須。良く書きます |
#[from] |
ほぼ必須2。他のクレートの型を内包したい時などに良く使います |
#[source] |
#[from] で事足りるため使ったことないですね... |
#[backtrace] |
nightlyなので使ったことがなく、今回の記事で初めて試しました |
これだけだとイマイチピンと来ませんね...マクロを理解したければ、実装を覗いてみるのも手かもしれませんが、マクロの展開結果を読んでみるのが手っ取り早いでしょう。
というわけで、全機能1を詰め込んだ次のコードを材料として用意しました!
#![feature(error_generic_member_access)]
use std::backtrace::Backtrace;
use std::error::request_ref;
#[derive(thiserror::Error, Debug)]
#[error("A super error occurred")]
struct SuperError(Backtrace);
#[derive(thiserror::Error, Debug)]
#[error("A wrapped error occurred: {super_}")]
struct WrappedError {
#[from]
super_: SuperError,
backtrace: Backtrace,
}
#[derive(thiserror::Error, Debug)]
enum MyError {
#[error(transparent)]
Wrapped(
#[from]
#[backtrace]
WrappedError,
),
#[error(transparent)]
Other(
#[from]
#[backtrace]
anyhow::Error,
),
}
fn func1() -> Result<(), SuperError> {
Err(SuperError(Backtrace::capture()))
}
fn func2() -> Result<(), WrappedError> {
func1()?;
Ok(())
}
fn func3() -> Result<(), MyError> {
func2()?;
Ok(())
}
fn func4() -> anyhow::Result<()> {
func1()?;
Ok(())
}
fn func5() -> Result<(), MyError> {
func4()?;
Ok(())
}
fn main() {
match func3() {
Err(e) => {
println!("Error from func3: {:#}", e);
if let Some(backtrace) = request_ref::<Backtrace>(&e) {
println!("Backtrace from func3:\n{}", backtrace);
}
}
Ok(_) => println!("func3 succeeded"),
}
match func5() {
Err(e) => {
println!("Error from func5: {:#}", e);
if let Some(backtrace) = request_ref::<Backtrace>(&e) {
println!("Backtrace from func5:\n{}", backtrace);
}
}
Ok(_) => println!("func5 succeeded"),
}
}
ちなみに実行結果は以下のような感じです。バックトレースがしっかりフォワーディングされていることがわかります。
❯ RUST_BACKTRACE=1 cargo run -q
Error from func3: A wrapped error occurred: A super error occurred
Backtrace from func3:
0: thiserror_expand::func1
at ./src/main.rs:35:20
1: thiserror_expand::func2
at ./src/main.rs:39:5
2: thiserror_expand::func3
at ./src/main.rs:44:5
3: thiserror_expand::main
at ./src/main.rs:59:11
4: ...省略...
Error from func5: A super error occurred
Backtrace from func5:
0: thiserror_expand::func1
at ./src/main.rs:35:20
1: thiserror_expand::func4
at ./src/main.rs:49:5
2: thiserror_expand::func5
at ./src/main.rs:54:5
3: thiserror_expand::main
at ./src/main.rs:69:11
4: ...省略...
これをcargo expandで展開してみます!読みやすくするために属性は消去しておきます。
use std::backtrace::Backtrace;
use std::error::request_ref;
struct SuperError(Backtrace);
impl ::thiserror::__private17::Error for SuperError {
// SuperError.0 が Backtrace なので、provideで提供
fn provide<'_request>(&'_request self, request: &mut ::core::error::Request<'_request>) {
request.provide_ref::<::thiserror::__private17::Backtrace>(&self.0);
}
}
// thiserror による #[error(...)] を元にしたDisplay実装
impl ::core::fmt::Display for SuperError {
fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
let Self(_0) = self;
__formatter.write_str("A super error occurred")
}
}
// #[derive(Debug)] マクロ展開
impl ::core::fmt::Debug for SuperError {
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "SuperError", &&self.0)
}
}
struct WrappedError {
super_: SuperError,
backtrace: Backtrace,
}
impl ::thiserror::__private17::Error for WrappedError {
// #[from] により source() が実装される
fn source(&self) -> ::core::option::Option<&(dyn ::thiserror::__private17::Error + 'static)> {
use ::thiserror::__private17::AsDynError as _;
::core::option::Option::Some(self.super_.as_dyn_error())
}
// Backtrace が検知されて provide() が実装される
fn provide<'_request>(&'_request self, request: &mut ::core::error::Request<'_request>) {
use ::thiserror::__private17::ThiserrorProvide as _;
self.super_.thiserror_provide(request);
request.provide_ref::<::thiserror::__private17::Backtrace>(&self.backtrace);
}
}
// thiserror による #[error(...)] を元にしたDisplay実装
impl ::core::fmt::Display for WrappedError {
fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
use ::thiserror::__private17::AsDisplay as _;
let Self { super_, backtrace } = self;
match (super_.as_display(),) {
(__display_super_,) => __formatter.write_fmt(format_args!(
"A wrapped error occurred: {0}",
__display_super_
)),
}
}
}
// thiserror による #[from] を元にした SuperError からの From実装
impl ::core::convert::From<SuperError> for WrappedError {
fn from(source: SuperError) -> Self {
WrappedError {
super_: source,
backtrace: ::core::convert::From::from(::thiserror::__private17::Backtrace::capture()),
}
}
}
// #[derive(Debug)] マクロ展開
impl ::core::fmt::Debug for WrappedError {
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(
f,
"WrappedError",
"super_",
&self.super_,
"backtrace",
&&self.backtrace,
)
}
}
enum MyError {
Wrapped(WrappedError),
Other(anyhow::Error),
}
impl ::thiserror::__private17::Error for MyError {
// #[from] により source() が実装される
fn source(&self) -> ::core::option::Option<&(dyn ::thiserror::__private17::Error + 'static)> {
use ::thiserror::__private17::AsDynError as _;
match self {
MyError::Wrapped { 0: transparent } => {
::thiserror::__private17::Error::source(transparent.as_dyn_error())
}
MyError::Other { 0: transparent } => {
::thiserror::__private17::Error::source(transparent.as_dyn_error())
}
}
}
// #[backtrace] により provide() が実装される
fn provide<'_request>(&'_request self, request: &mut ::core::error::Request<'_request>) {
match self {
MyError::Wrapped { 0: source, .. } => {
use ::thiserror::__private17::ThiserrorProvide as _;
source.thiserror_provide(request);
}
MyError::Other { 0: source, .. } => {
use ::thiserror::__private17::ThiserrorProvide as _;
source.thiserror_provide(request);
}
}
}
}
// thiserror による #[error(...)] を元にしたDisplay実装
impl ::core::fmt::Display for MyError {
fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
MyError::Wrapped(_0) => ::core::fmt::Display::fmt(_0, __formatter),
MyError::Other(_0) => ::core::fmt::Display::fmt(_0, __formatter),
}
}
}
// thiserror による #[from] を元にした WrappedError からの From実装
impl ::core::convert::From<WrappedError> for MyError {
fn from(source: WrappedError) -> Self {
MyError::Wrapped { 0: source }
}
}
// thiserror による #[from] を元にした anyhow::Error からの From実装
impl ::core::convert::From<anyhow::Error> for MyError {
fn from(source: anyhow::Error) -> Self {
MyError::Other { 0: source }
}
}
// #[derive(Debug)] マクロ展開
impl ::core::fmt::Debug for MyError {
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
MyError::Wrapped(__self_0) => {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Wrapped", &__self_0)
}
MyError::Other(__self_0) => {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Other", &__self_0)
}
}
}
}
すべての impl ブロックについて展開理由は明らかになっていますね!前回内容と照らし合わせると各ブロック・実装されるメソッドの役割・必要性は簡単に説明できます。
やはり thiserror::Error を知るには std::error::Error を知ればよいというのは正しかったようです!
From トレイトだけその必要性を説明していませんでした。こちらは ? 演算子と相性が良いために設けられていると考えるのが妥当でしょう。
? 演算子の脱糖は以下と同等とみなすことができます2。
match 対象式 {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
}
From::from(e) の部分について、 #[from] 属性の効果により From の実装が行われると捗るというわけです!
前回の忘れ物: fn main() -> Result<(), Box<dyn std::error::Error>>
本当は前回言及しておくべきだったのですがすっかり忘れていたのでここで触れようと思います。
前回記事では Result<T, E> の E はなんでも良いけど、そうは言っても必要最低限 std::error::Error: Debug + Display が実装されていてほしいという話をしました。そして今回、それは thiserror::Error deriveマクロによって簡単に実装できるという話をしました。
こうした最低限のドレスコードを持ったエラー型というのは頻出で、トレイトオブジェクト Box<dyn std::error::Error> の形で使われることがしばしばあります!
特に良く使われる場所としては main 関数の返り値の E を Box<dyn std::error::Error> にするなどでしょうか?
fn main() -> Result<(), Box<dyn std::error::Error>> {
let n = std::env::args()
.nth(1)
.ok_or("Please provide a number as an argument")?
.parse::<i64>()?;
println!("{}", n + 10);
Ok(())
}
-
Eの型に困った - 特にエラー内容で分岐することはなく一番上まで持っていく
このような場合はとりあえず Result<T, Box<dyn std::error::Error>> としておけば特に問題なさそうです。ちなみに上位互換となるので anyhow::Error だとなお良いでしょう!こちらは次回触れたいと思います。
まとめ・所感
というわけで、hooqアドベントカレンダー 21日目の記事でした!
hooqはメソッドを ? 演算子にフックする属性マクロです。本アドカレではhooqの使い方を始め、hooqマクロを作成するにあたり得た知識、Rustのエラーハンドリング・エラーロギング周りの話をまとめています。
次回からanyhowやtracingなど具体的なエラー関連クレートを見ていきますが、プリミティブ的な部分については今回までで一通り抑えられたと思います!
ここまで読んでいただきありがとうございました。
-
ただし
.0や.varでフィールドアクセスできる等の細かい機能については省略しています。 ↩ -
【Rust】?演算子を再発明する #hooq - Qiita も良かったら読んでみてください! ↩