Reputation: 45
I'm writing a new crate, and I want it to be useable with any implementation of a trait (defined in another crate). The trait looks something like this:
pub trait Trait {
type Error;
...
}
I have my own Error
type, but sometimes I just want to forward the underlying error unmodified. My instinct is to define a type like this:
pub enum Error<T: Trait> {
TraitError(T::Error),
...
}
This is similar to the pattern encouraged by thiserror, and appears to be idiomatic. It works fine, but I also want to use ?
in my implementation, so I need to implement From
:
impl<T: Trait> From<T::Error> for Error<T> {
fn from(e: T::Error) -> Self { Self::TraitError(e) }
}
That fails, because it conflicts with impl<T> core::convert::From<T> for T
. I think I understand why — some other implementor of Trait
could set type Error = my_crate::Error
such that both impl
s would apply — but how else can I achieve similar semantics?
I've looked at a few other crates, and they seem to handle this by making their Error
(or equivalent) generic over the error type itself, rather than the trait implementation. That works, of course, but:
T
actually implements multiple traits, each with their own Error
types, so I'd now have to return types like Result<..., Error<<T as TraitA>::Error, <T as TraitB>::Error>>
etc;Trait
is lost).Is making my Error
generic over the individual types the best (most idiomatic) option today?
Upvotes: 4
Views: 940
Reputation: 2177
Instead of implementing From
for your Error
enum, consider instead using Result::map_err
in combination with ?
to specify which variant to return.
This works even for enums generic over a type using associated types, as such:
trait TraitA {
type Error;
fn do_stuff(&self) -> Result<(), Self::Error>;
}
trait TraitB {
type Error;
fn do_other_stuff(&self) -> Result<(), Self::Error>;
}
enum Error<T: TraitA + TraitB> {
DoStuff(<T as TraitA>::Error),
DoOtherStuff(<T as TraitB>::Error),
}
fn my_function<T: TraitA + TraitB>(t: T) -> Result<(), Error<T>> {
t.do_stuff().map_err(Error::DoStuff)?;
t.do_other_stuff().map_err(Error::DoOtherStuff)?;
Ok(())
}
Here the important bits are that Error
has no From
implementations (aside from blanket ones), and that the variant is specified using map_err
. This works as Error::DoStuff
can be interpreted as a fn(<T as TraitA>::Error) -> Error
when passed to map_err
. Same happens with Error::DoOtherStuff
.
This approach is scaleable with however many variants Error
has and whether or not they are the same type. It might also be clearer to someone reading the function, as they can figure out that a certain error comes from a certain place without needing to check From
implementations and where the type being converted from appears in the function.
Upvotes: 2