Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I have a collection of objects that differ by their associated type?

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!

like image 696
Ben S Avatar asked Mar 08 '15 22:03

Ben S


1 Answers

When you write an impl Check and specialize your type Error with a concrete type, you are ending up with different types.

In other words, Check<Error = NegativeError> and Check<Error = EvenError> are statically different types. Although you might expect Check<Error> to describe both, note that in Rust NegativeError and EvenError are not sub-types of Error. They are guaranteed to implement all methods defined by the Error trait, but then calls to those methods will be statically dispatched to physically different functions that the compiler creates (each will have a version for NegativeError, one for EvenError).

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 that Vec requires its types to be homogeneous (you can't have a vec![1u8, 'a'] either, although a char is representable as a u8 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...

  1. You might find it much easier if you used the Error trait in std::error instead of your own version of it.

    You may need to impl Display to create a description with a dynamically built String, like so:

    impl fmt::Display for EvenError {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "{} is even", self.number)
        }
    }
    
    impl Error for EvenError {
        fn description(&self) -> &str { "even error" }
    }
    
  2. Now you can drop the associated type and have Check return a trait object:

    trait Check  {
        fn check_number(&self, number: i32) -> Option<Box<Error>>;
    }
    

    your Vec now has an expressible type:

    let mut checks: Vec<Box<Check>> = vec![
        Box::new(EvenCheck) ,
        Box::new(NegativeCheck) ,
    ];
    
  3. 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 concrete Error type out of your trait object.

    for number in numbers {
        for check in &mut checks {
            if let Some(error) = check.check_number(number) {
                println!("{}", error);
    
                if let Some(s_err)= error.downcast_ref::<EvenError>() {
                    println!("custom logic for EvenErr: {} - {}", s_err.number, s_err)                    
                }
            }
        }
    }
    

full example on the playground

like image 61
Paolo Falabella Avatar answered Sep 17 '22 12:09

Paolo Falabella