Search code examples
genericsrustdynamictraitsupcasting

Casting a struct to a trait object with associated types in Rust


In rust, you can have a trait, implement it into a struct, and upcast your struct to a trait object :

trait T {}

struct S {}
impl T for S {}

fn main() {
    let s: S = S {};
    let s_as_t: &dyn T = &s;
}

This is an incredibly useful feature, because if I have multiple objects which all implement the trait T, I can now put them all in a single array of type Vec<Box<dyn T>>, and define global behaviors really easily by calling a function on each element.

BUT

How do I do the same thing when my original trait also has an associated type ?

This works really well, no pb :

trait T_Subtype {}

trait T {
    type subtype: T_Subtype;
}

struct S {}
impl T_Subtype for S {}
impl T for S {
    type subtype = S;
}

fn main() {
    let s: S = S {};
    let s_as_t: &dyn T<subtype = S> = &s;
}

But I can't find any way to upcast the associated type, the following code cannot compile :

trait T_Subtype {}

trait T {
    type subtype: T_Subtype;
}

struct S {}
impl T_Subtype for S {}
impl T for S {
    type subtype = S;
}

fn main() {
    let s: S = S {};
    let s_as_t: &dyn T<subtype = dyn T_Subtype> = &s; // only line that changes
}

Without this feature, I cannot put (this is an illustration) multiple structs S1 S2 and S3, that all implement T but might have a different subtype, in a single array, and I have to define global behaviors for each subtype, making it really hard to maintain (especially if there are multiple subtypes), even though the function I want to call on all of them is defined !


Solution

  • Ok, so this problem hurt my brain for a while. Apparently it is impossible to do, but I still felt like there should be a way. And here it is :

    // we define the traits and sub-traits, like we did before
    trait T2 {
        fn hello2(&self);
    }
    trait T1 {
        type SubT: T2;
    
        fn hello1(&self) -> Self::SubT;
    }
    
    // first implementation of T1 (maybe somewhere else in the code)
    struct S1;
    impl T2 for S1 {
        fn hello2(&self) {
            println!("hello2 from S1");
        }
    }
    impl T1 for S1 {
        type SubT = S1;
    
        fn hello1(&self) -> Self::SubT {
            println!("hello from s1");
            S1 {}
        }
    }
    
    // second implementation of T1 (maybe somewhere else in the code)
    struct S2;
    impl T2 for S2 {
        fn hello2(&self) {
            println!("hello2 from S2");
        }
    }
    impl T1 for S2 {
        type SubT = S2;
    
        fn hello1(&self) -> Self::SubT {
            println!("hello from s2");
            S2 {}
        }
    }
    
    // where the magic happens !!
    // we use a blanket implementation to make it automatic
    trait T1Blanket {
        fn hello1(&self) -> Box<dyn T2>;
    }
    impl<S: T1> T1Blanket for S where S::SubT: 'static {
        fn hello1(&self) -> Box<dyn T2> {
            Box::from(self.hello1()) as Box<dyn T2>
        }
    }
    
    // and now we can use it easily
    fn main() {
        let s1 = S1 {};
        let s2 = S2 {};
    
        let array: Vec<Box<dyn T1Blanket>> = vec![Box::from(s1), Box::from(s2)];
        for element in array {
            let h = element.hello1(); // prints "hello from s1" and "hello from s2" successively
            h.hello2(); // you can also call the other trait method's
        }
    }
    

    This works beautifully, and the best part is once you created the blanket implementation, you don't have to touch anything ! You can create as many structs implementing T1 and T2 as you want, and they won't have any restriction put on them (they won't need special function to be implemented either).

    You can even automate this work using macros if you have many of such traits : this crate apparently does it for you.

    I'm still incredibly surprised that this isn't included directly in the default behaviors of trait objects which have associated types. As long as the trait implementation isn't needed to call the function, using trait objects shouldn't be a problem.