cargo-expand を使ってみよう
前回は thiserror
crate の基本的な使い方について書きました 。今回は cargo-expand
の使い方を確認しつつ、 thiserror
crate の提供する derive macro の展開結果を見ていきます。
cargo-expand
とは
cargo-expand
は macro の展開結果を出力するコマンドです。
https://github.com/dtolnay/cargo-expand
インストール方法
cargo install cargo-expand
README によると rustfmt
もあれば使うらしいです。 cargo
が入っていて rustfmt
が入っていない環境はそんなにないはず…… (知らない) 。
README の例
README の例をそのまま実行してみます。
ファイルを用意して。
#[derive(Debug)]
struct S;
fn main() {
println!("{:?}", S);
}
cargo expand
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
struct S;
#[automatically_derived]
impl ::core::fmt::Debug for S {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f, "S")
}
}
fn main() {
{
::std::io::_print(format_args!("{0:?}\n", S));
};
}
ちょっと README とは違う結果ですけど、エディションの違いなどによるものでしょう。
大切なのは #[derive(Debug)]
や println!
の展開された結果が表示されていることが分かります。
その他のオプション
cargo expand --help
してください。省略。
前回の記事の例
さて、本題。前回の thiserror
の記事の例に対して cargo expand
して展開した結果を見てみましょう。
前回の記事の例 1
まずはこれ。
#[derive(Debug, thiserror::Error)]
#[error("unit struct error")]
struct UnitStructError;
cargo expand
すると……
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
#[error("unit struct error")]
struct UnitStructError;
#[automatically_derived]
impl ::core::fmt::Debug for UnitStructError {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f, "UnitStructError")
}
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::thiserror::__private::Error for UnitStructError {}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::core::fmt::Display for UnitStructError {
#[allow(clippy::used_underscore_binding)]
fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
#[allow(unused_variables, deprecated)]
let Self {} = self;
__formatter.write_str("unit struct error")
}
}
#[derive(...)]
はなくなるものの #[error("...")]
は消えないみたいですね。
#[derive(...)]
の展開結果は #[automatically_derived]
が付くんですかね。
まずは Debug
trait の実装。これは割愛します。また Debug
trait のこともそのうち書くと思うので、そのときにでも。
次は impl ::thiserror::__private::Error for TupleStructError
ですね。 ::thiserror::__private::Error
ってなんだよって話ですけど、 https://github.com/dtolnay/thiserror/blob/7a5fbc65c03e39eed9a3fd2cd48e4313178734ed/src/lib.rs#L299 という thiserror
のソースコードを見れば std::error::Error
を re-export したものだと分かります。中身は空でデフォルト実装を使うだけのものですね。
そして Display
trait の実装。 formatter
の write_str
に #[error("...")]
で指定した内容を書いているだけですね。
期待通りの出力ですね。
前回の記事の例 2
次はどうでしょう。
#[derive(Debug, thiserror::Error)]
#[error("tuple struct error: {0}")]
struct TupleStructError(#[from] std::io::Error);
cargo expand
すると……
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
#[error("tuple struct error: {0}")]
struct TupleStructError(#[from] std::io::Error);
#[automatically_derived]
impl ::core::fmt::Debug for TupleStructError {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_tuple_field1_finish(
f,
"TupleStructError",
&&self.0,
)
}
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::thiserror::__private::Error for TupleStructError {
fn source(
&self,
) -> ::core::option::Option<&(dyn ::thiserror::__private::Error + 'static)> {
use ::thiserror::__private::AsDynError as _;
::core::option::Option::Some(self.0.as_dyn_error())
}
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::core::fmt::Display for TupleStructError {
#[allow(clippy::used_underscore_binding)]
fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
use ::thiserror::__private::AsDisplay as _;
#[allow(unused_variables, deprecated)]
let Self(_0) = self;
match (_0.as_display(),) {
(__display0,) => {
__formatter
.write_fmt(format_args!("tuple struct error: {0}", __display0))
}
}
}
}
#[allow(
deprecated,
unused_qualifications,
clippy::elidable_lifetime_names,
clippy::needless_lifetimes,
)]
#[automatically_derived]
impl ::core::convert::From<std::io::Error> for TupleStructError {
fn from(source: std::io::Error) -> Self {
TupleStructError { 0: source }
}
}
#[from]
も #[error("...")]
と同様に残っています。
Debug
trait の実装は割愛。
Error
trait の実装。 #[from]
が #[source]
を兼ねることからきちんと source
メソッドを実装していますね。 source
の実装では self.0
を ::thiserror::__private::AsDynError
として .as_dyn_error()
を呼び、それを返していますね。名前や型から dyn Error
の参照を得るものですね。
Display
trait の実装。 self.0
を ::thiserror::__private::AsDisplay
として .as_display()
を呼び、 #[error("...")]
で指定した文字列の形式に .write_fmt(format_args!(...), ...)
していますね。
そして From
trait の実装。 #[from]
で指定した型から from
できるようにしてくれていますね。
いいですね。
前回の記事の例 3
どんどん行きましょう。次はこれです。
#[derive(Debug, thiserror::Error)]
#[error("struct error: {cause:?}")]
struct StructError {
#[source]
cause: std::io::Error,
}
cargo expand
すると……
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
#[error("struct error: {cause:?}")]
struct StructError {
#[source]
cause: std::io::Error,
}
#[automatically_derived]
impl ::core::fmt::Debug for StructError {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field1_finish(
f,
"StructError",
"cause",
&&self.cause,
)
}
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::thiserror::__private::Error for StructError {
fn source(
&self,
) -> ::core::option::Option<&(dyn ::thiserror::__private::Error + 'static)> {
use ::thiserror::__private::AsDynError as _;
::core::option::Option::Some(self.cause.as_dyn_error())
}
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::core::fmt::Display for StructError {
#[allow(clippy::used_underscore_binding)]
fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
#[allow(unused_variables, deprecated)]
let Self { cause } = self;
match (cause,) {
(__field_cause,) => {
__formatter.write_fmt(format_args!("struct error: {0:?}", __field_cause))
}
}
}
}
Debug
trait の実装は割愛。
Error
trait の実装は例 2 と変わらず。 #[source]
がついているから .source()
が実装されていますね。
Display
trait の実装は format が {0:?}
になったくらいですね。フィールドの展開などは thiserror の実装がいろいろやってくれているんでしょうか (きちんとソースコードは追っていないので想像ですが) 。展開後のコードは素朴ですね。
#[from]
がないので From
trait の実装はなく、ややスッキリしていますね。
前回の記事の例 4
最後の例ですね。
#[derive(Debug, thiserror::Error)]
enum EnumError {
#[error("io error")]
Io(#[source] std::io::Error),
#[error(transparent)]
From(#[from] Box<dyn std::error::Error + Send + Sync>),
#[error(transparent)]
Unknown(Box<dyn std::error::Error + Send + Sync>),
}
cargo expand
すると……
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
enum EnumError {
#[error("io error")]
Io(#[source] std::io::Error),
#[error(transparent)]
From(#[from] Box<dyn std::error::Error + Send + Sync>),
#[error(transparent)]
Unknown(Box<dyn std::error::Error + Send + Sync>),
}
#[automatically_derived]
impl ::core::fmt::Debug for EnumError {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
EnumError::Io(__self_0) => {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Io", &__self_0)
}
EnumError::From(__self_0) => {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "From", &__self_0)
}
EnumError::Unknown(__self_0) => {
::core::fmt::Formatter::debug_tuple_field1_finish(
f,
"Unknown",
&__self_0,
)
}
}
}
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::thiserror::__private::Error for EnumError {
fn source(
&self,
) -> ::core::option::Option<&(dyn ::thiserror::__private::Error + 'static)> {
use ::thiserror::__private::AsDynError as _;
#[allow(deprecated)]
match self {
EnumError::Io { 0: source, .. } => {
::core::option::Option::Some(source.as_dyn_error())
}
EnumError::From { 0: transparent } => {
::thiserror::__private::Error::source(transparent.as_dyn_error())
}
EnumError::Unknown { 0: transparent } => {
::thiserror::__private::Error::source(transparent.as_dyn_error())
}
}
}
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::core::fmt::Display for EnumError {
fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
#[allow(unused_variables, deprecated, clippy::used_underscore_binding)]
match self {
EnumError::Io(_0) => __formatter.write_str("io error"),
EnumError::From(_0) => ::core::fmt::Display::fmt(_0, __formatter),
EnumError::Unknown(_0) => ::core::fmt::Display::fmt(_0, __formatter),
}
}
}
#[allow(
deprecated,
unused_qualifications,
clippy::elidable_lifetime_names,
clippy::needless_lifetimes,
)]
#[automatically_derived]
impl ::core::convert::From<Box<dyn std::error::Error + Send + Sync>> for EnumError {
fn from(source: Box<dyn std::error::Error + Send + Sync>) -> Self {
EnumError::From { 0: source }
}
}
Debug
trait の実装。割愛。
Error
trait の実装。 match
で variant ごとに分けていますね。 #[transparent]
については Some(source.as_dyn_error())
とせず Error::source(transparent.as_dyn_error())
として、中身の Error
に丸投げしていますね。確かに透過的です。
Display
trait の実装。こちらも match
で variant ごとに分けていますね。こちらも #[transparent]
について Display::fmt(_0, __formatter)
として丸投げしていますね。うんうん。
From
trait の実装。 #[from]
をつけたのでこれもありますね。 #[from]
を付けた EnumError::From
variant がきちんと指定されています。
もう見慣れてきましたね。
おわり
cargo-expand
の紹介と、それを使って前回の thiserror
の記事の例の macro を展開してみました。
macro は何が起きるかパッと見では分かりづらいですが、よく使うものくらいは展開された中身を見てみる・理解してみると面白いかもしれませんね。
Discussion