I have a program that involves examining a complex data structure to see if it has any defects. (It's quite complicated, so I'm posting example code.) All of the checks are unrelated to each other, and will all have their own modules and tests.
More importantly, each check has its own error type that contains different information about how the check failed for each number. I'm doing it this way instead of just returning an error string so I can test the errors (it's why Error
relies on PartialEq
).
My Code So Far
I have traits for Check
and Error
:
trait Check {
type Error;
fn check_number(&self, number: i32) -> Option<Self::Error>;
}
trait Error: std::fmt::Debug + PartialEq {
fn description(&self) -> String;
}
And two example checks, with their error structs. In this example, I want to show errors if a number is negative or even:
#[derive(PartialEq, Debug)]
struct EvenError {
number: i32,
}
struct EvenCheck;
impl Check for EvenCheck {
type Error = EvenError;
fn check_number(&self, number: i32) -> Option<EvenError> {
if number < 0 {
Some(EvenError { number: number })
} else {
None
}
}
}
impl Error for EvenError {
fn description(&self) -> String {
format!("{} is even", self.number)
}
}
#[derive(PartialEq, Debug)]
struct NegativeError {
number: i32,
}
struct NegativeCheck;
impl Check for NegativeCheck {
type Error = NegativeError;
fn check_number(&self, number: i32) -> Option<NegativeError> {
if number < 0 {
Some(NegativeError { number: number })
} else {
None
}
}
}
impl Error for NegativeError {
fn description(&self) -> String {
format!("{} is negative", self.number)
}
}
I know that in this example, the two structs look identical, but in my code, there are many different structs, so I can't merge them. Lastly, an example main
function, to illustrate the kind of thing I want to do:
fn main() {
let numbers = vec![1, -4, 64, -25];
let checks = vec![
Box::new(EvenCheck) as Box<Check<Error = Error>>,
Box::new(NegativeCheck) as Box<Check<Error = Error>>,
]; // What should I put for this Vec's type?
for number in numbers {
for check in checks {
if let Some(error) = check.check_number(number) {
println!("{:?} - {}", error, error.description())
}
}
}
}
You can see the code in the Rust playground.
Solutions I've Tried
The closest thing I've come to a solution is to remove the associated types and have the checks return Option<Box<Error>>
. However, I get this error instead:
error[E0038]: the trait `Error` cannot be made into an object
--> src/main.rs:4:55
|
4 | fn check_number(&self, number: i32) -> Option<Box<Error>>;
| ^^^^^ the trait `Error` cannot be made into an object
|
= note: the trait cannot use `Self` as a type parameter in the supertraits or where-clauses
because of the PartialEq
in the Error
trait. Rust has been great to me thus far, and I really hope I'm able to bend the type system into supporting something like this!
I eventually found a way to do it that I'm happy with. Instead of having a vector of
Box<Check<???>>
objects, have a vector of closures that all have the same type, abstracting away the very functions that get called:Not only does this allow for a vector of
Box<Error>
objects to be returned, it allows theCheck
objects to provide their own Error associated type which doesn't need to implementPartialEq
. The multipleas
es look a little messy, but on the whole it's not that bad.When you write an
impl Check
and specialize yourtype Error
with a concrete type, you are ending up with different types.In other words,
Check<Error = NegativeError>
andCheck<Error = EvenError>
are statically different types. Although you might expectCheck<Error>
to describe both, note that in RustNegativeError
andEvenError
are not sub-types ofError
. They are guaranteed to implement all methods defined by theError
trait, but then calls to those methods will be statically dispatched to physically different functions that the compiler creates (each will have a version forNegativeError
, one forEvenError
).Therefore, you can't put them in the same
Vec
, even boxed (as you discovered). It's not so much a matter of knowing how much space to allocate, it's thatVec
requires its types to be homogeneous (you can't have avec![1u8, 'a']
either, although achar
is representable as au8
in memory).Rust's way to "erase" some of the type information and gain the dynamic dispatch part of subtyping is, as you discovered, trait objects.
If you want to give another try to the trait object approach, you might find it more appealing with a few tweaks...
You might find it much easier if you used the
Error
trait instd::error
instead of your own version of it.You may need to
impl Display
to create a description with a dynamically builtString
, like so:Now you can drop the associated type and have
Check
return a trait object:your
Vec
now has an expressible type:The best part of using
std::error::Error
...is that now you don't need to use
PartialEq
to understand what error was thrown.Error
has various types of downcasts and type checks if you do need to retrieve the concreteError
type out of your trait object.full example on the playground
I'd suggest you some refactoring.
First, I'm pretty sure, that vectors should be homogeneous in Rust, so there is no way to supply elements of different types for them. Also you cannot downcast traits to reduce them to a common base trait (as I remember, there was a question about it on SO).
So I'd use algebraic type with explicit match for this task, like this:
As for error handling, consider use FromError framework, so you will able to involve try! macro in your code and to convert error types from one to another.