Heterogeneous collection


#1

Hi, I’m new in Rust and I’m doing a code test to be familiar with the language.

In the code test I have a method which return a heterogenous collection of “BoardingPass” (it could be a bus, train or any other type). Here is the code:

pub fn generate<'a>(&'a self) -> Vec<Box<BoardingPass + 'a>> {
    let mut vector: Vec<Box<BoardingPass>> = Vec::new();

    let madrid_barna = BoardingPassTrain::new("1", &self.madrid, &self.barna);
    vector.push(Box::new(madrid_barna));

    let barna_madrid = BoardingPassTrain::new("2", &self.barna, &self.madrid);
    vector.push(Box::new(barna_madrid));

    vector
}

The BoardingPass trait I would like to inherit from Hash + PartialEq in order to let me sort the collection easily.

pub trait BoardingPass : Hash + PartialEq + Debug {
    fn boarding_id(&self) -> &str;
    fn city_from(&self) -> &City;
    fn city_to(&self) -> &City;
}

That produce the following compilation error:

src/sort/boarding_pass_generator.rs:18:46: 18:63 error: the trait `models::boarding_pass::BoardingPass` cannot be made into an object [E0038]
src/sort/boarding_pass_generator.rs:18     pub fn generate<'a>(&'a self) -> Vec<Box<BoardingPass + 'a>> {
                                                                                    ^~~~~~~~~~~~~~~~~
src/sort/boarding_pass_generator.rs:18:46: 18:63 help: run `rustc --explain E0038` to see a detailed explanation
src/sort/boarding_pass_generator.rs:18:46: 18:63 note: the trait cannot use `Self` as a type parameter in the super trait listing

I’m not 100% sure about what the error means, but I guess it could be related to the heterogenous collection with PartialEq Trait due to we can’t probably check equality for BoardingPassBus = BoardingPassTrain because the types are different.

I would like to add a default implementation of PartialEq for the BoardingPass Trait, something like:

impl PartialEq for BoardingPass {
    fn eq(&self, other: &BoardingPass) -> bool {
        self.boarding_id() == other.boarding_id()
    }
}

I did this same exercise in Swift and I had to create a wrapper AnyBoardingPass to work around this issue. Which options can I have in Rust? - Probably I could do the same, but I would like to explore other alternatives that probably I’m not aware of.

Thank you


#2

Ok, here’s the code (also on playpen):

use std::fmt::Debug;

pub struct City;

pub trait BoardingPass: Debug {
    fn boarding_id(&self) -> &str;
    fn city_from(&self) -> &City;
    fn city_to(&self) -> &City;
}

impl<'a, 'b> PartialEq<BoardingPass + 'b> for BoardingPass + 'a {
    fn eq(&self, other: &(BoardingPass + 'b)) -> bool {
        self.boarding_id() == other.boarding_id()
    }
}

#[allow(dead_code)]
fn test(a: &BoardingPass, b: &BoardingPass) {
    let _ = a == b;
}

fn main() {}

So.

The first problem is, as the compiler says, you can’t use Self in supertraits. The problem is that PartialEq has an optional generic parameter (Rhs) which defaults to Self. So what you’d actually written was:

pub trait BoardingPass : Hash + PartialEq<Self> + Debug ...

So PartialEq has to go.

The next problem is Hash, because that has type-generic methods, which cannot be added to a vtable and consequently prohibit creating an object type.

So Hash has to go as well.

At that point, the remaining problem is implementing PartialEq on the trait. You’ll immediately run into problems with your naïve implementation, because again, Rust is trying to help you. Your impl actually looks like this:

impl PartialEq<Self + 'static> for BoardingPass + 'static {
    fn eq<'a, 'b>(&'a self, other: &'b (BoardingPass + 'b)) -> bool {
        self.boarding_id() == other.boarding_id()
    }
}

The short version is: when you treat a trait as a type, there must be a limiting lifetime. If it’s behind a borrowed pointer, the lifetime is inferred to be whatever the lifetime of the borrowed pointer is (like &'b (BoardingPass + 'b)); otherwise, it’s 'static (like BoardingPass + 'static).

This means that the other argument doesn’t match what you said it was going to be. Even if you fix that by changing the signature of eq, you end up with a PartialEq implementation that only works on static BoardingPasses, which you will almost never have.

The solution is to just write a maximally lifetime-generic PartialEq implementation.

Phew. I think that’s everything.

As for getting Hash back, the simplest thing is probably to define a method like fn hash_code(&self) -> u64, then implement Hash in terms of that on the trait.