Reputation: 147
Currently, I'm working on a project where I use several crates that return different errors. I'm trying to not use unwrap on a result but instead passing the error upwards using the question mark syntax. To be able to do this I created my own error enum that has variants for the different types of errors from the different crates I'm using and then use map_err to map the errors to my error enum. I also decided that I should add the line and file where I remapped the error so I could see where I encountered the error.
My error enum#[derive(Debug, Clone)]
pub enum Error {
GameError(ggez::GameError, String),
RonError(ron::error::Error, String),
}
Example of mapping an error
let rdr = filesystem::open(ctx, gen_conf_path)
.map_err(|e| {
Error::GameError(e, format!("at line {} in {}", line!(), file!()))
})?;
The problem with that is that the closure I need to send to map_err
becomes quite long and is essentially the same every time except for the enum variant I'm mapping to. So I want to create a macro that generates the closure for the correct enum variant. So i could write something like the following.
let rdr = filesystem::open(ctx, gen_conf_path).map_err(map_to!(Error::GameError))?
And the map_to!
macro would generate the code i had before.
Is this possible? Can I send an enum variant to a macro and let it construct it or should I do this in a completely different way?
Upvotes: 0
Views: 1214
Reputation: 43753
I'd like to suggest a different path for the problem you started with. All enum variants having a field which is the same purpose (error location) suggests that you ought to split it out into a struct. (For example, std::io::Error
has a struct Error
and an enum ErrorKind
.) Besides conceptual deduplication, this also means:
match
all the variants to find the common information. (Though you can add methods to the enum to handle this.)#[derive(Debug, Clone)]
struct LocatedError {
kind: AnyError,
location: String,
}
#[derive(Debug, Clone)]
enum AnyError {
GameError(ggez::GameError),
RonError(ron::error::Error),
}
It happens that we've turned this into what might as well be two independent error types that are nested. This is useful, because we can now use From
implementations in the usual recommended fashion to convert to AnyError
:
impl From<ggez::GameError> for AnyError {
fn from(error: ggez::GameError) -> Self {
Self::GameError(error)
}
}
// And the same for RonError
(By the way, the above From
implementation can be generated for you by the thiserror
derive macro crate.)
Now that we have conversion handled, the macro can primarily concern itself with the location generation, and not at all with the error type (because trait implementation selection handles that):
impl LocatedError {
fn new(error: impl Into<AnyError>, location: String) -> Self {
Self {
kind: error.into(),
location,
}
}
}
macro_rules! err_here {
() => {
|e| {
LocatedError::new(e, format!("at line {} in {}", line!(), file!()))
}
};
}
Usage of this version in your example code:
let rdr = filesystem::open(ctx, gen_conf_path).map_err(err_here!())?;
Note that we do not have to pass the enum variant to the macro.
Upvotes: 1
Reputation: 26157
An interesting implementation detail about enum variants, is that the initializers are actually functions.
We have another useful pattern that exploits an implementation detail of tuple structs and tuple-struct enum variants. These types use
()
as initializer syntax, which looks like a function call. The initializers are actually implemented as functions returning an instance that’s constructed from their arguments. We can use these initializer functions as function pointers that implement the closure traits, which means we can specify the initializer functions as arguments for methods that take closures– Advanced Functions and Closures - The Rust Programming Language
This means, that if you had an enum FooBar
, which had a variant Foo(i32, i32)
, then you could use and pass FooBar::Foo
as a Fn(i32, i32) -> FooBar
.
enum FooBar {
Foo(i32, i32),
Bar(String),
}
fn foo(f: fn(i32, i32) -> FooBar) -> FooBar {
f(1, 2)
}
fn bar<F>(f: F) -> FooBar
where
F: Fn(i32, i32) -> FooBar,
{
f(1, 2)
}
fn main() {
foo(FooBar::Foo);
bar(FooBar::Foo);
}
Thus if you think of your enum variant as function, then your macro becomes quite simple:
macro_rules! map_to {
($f:expr) => {
|e| {
$f(e, format!("at line {} in {}", line!(), file!()))
}
};
}
This is of course assuming that e
is always the valid type, in relation to the enum variant used in relation to map_to
.
Upvotes: 3