Mixing objects with associated types implementing same trait bound

Hi,

Suppose that I need to store in the same vector instances of different structures implementing the same trait with different associated type, but (the associated type) implementing the same trait... Ehm... Yeah, maybe it's better to show an example:

trait Action{
}

trait Rule{
    type A: Action;
}

struct FooAction;

impl Action for FooAction {
}

struct BarAction;

impl Action for BarAction {
}

struct FooRule;

impl Rule for FooRule {
    type A = FooAction;
}

struct BarRule;

impl Rule for BarRule {
    type A = BarAction;
}

fn main() {
    let foo = FooRule{};
    let bar = BarRule{};
    // let _v = vec![&foo, &bar]; <--- This doesn't work!
    // I was expecting that _v has type Vec<&dyn Rule<A=Action>>
}

How I can manage this situation? Why the compiler is complaining?

I am using references, they are stored as fat pointer (so they are sized and dynamically dispatchable) and their associated type is implementing Action as requested.

There are a few possible causes of confusion here. Some or all of these you probably already know:

  • Traits and types are distinct things
  • dyn Trait is a concrete type for object safe traits that
    • implementors of the trait Trait can coerce into
    • also implements the trait Trait
    • but there is no subtyping between dyn Trait and implementors of the trait Trait
  • The Ty in dyn Trait<Associated=Ty> is a specific concrete type, not a trait bound
    • but confusingly, you used to be able to just say Trait instead of dyn Trait where a type was expected
    • the Ty is also part of the overall type of dyn Trait<Associated=Ty>. That is, dyn Trait<Associated=i32> and dyn Trait<Associated=u64> are different types, for example.
    • You can't coerce a dyn Trait<Associated=Ty> into a dyn Trait<Associated=dyn Trait2>. (The implementation or trait definition may rely on the concrete associated type chosen.)

So, on to your code. Your comment says

    // let _v = vec![&foo, &bar]; <--- This doesn't work!
    // I was expecting that _v has type Vec<&dyn Rule<A=Action>>

Action here would need to be a specific concrete type; it can't be a trait bound. So dyn Rule<A=Action> would be more properly written today as a dyn Rule<A=dyn Action>. But those aren't what you have: you have an A=FooAction and a A=BarAction. If Action is object safe, FooAction and BarAction could be coerced into a dyn Action, but they are all three distinct types, and dyn Rule<A=FooAction> can't coerce into a dyn Rule<A=dyn Action>, say.


Here's a modification of your playground that gives one example why the associated type needs to be completely specified, and not just a trait bound.

Here's another with a couple approaches to using dyn Action in the associated type. This means all the implementors of Rule have to use the same associated type, though...

So here's one more showing a possible approach to "hide" the concrete associated type behind it's trait capabilities via another trait.

3 Likes

Thank you for your very detailed reply. :+1:

I got some points of your answer, while other are still not clear to me. Anyway, my bad, I need to review better some concepts around trait object, subtyping and associated type.

Honestly I was thinking that if an associated type is a dyn Trait it is ok to provide a pointer (reference, Box, Rc, ...) to an implementor of that trait, since the implementor can be coerced to the trait. But again... I need to get better this part.

Could I ask you where I can find a good detailed resource to review this part?

I don't have a good citation that covers everything, sadly. If you're coming from OO, this article may be useful from a practical POV. (Though I don't agree with some of their terminology, technically.)

I guess I would highlight

  • Traits are like interfaces (checked at compile time)
  • There is generally no subtyping of types1, e.g. there's no subtyping based on implementing traits (but there are various types of coercion)
  • dyn Trait is a concrete, static type; it can be coerced, and performs dynamic dispatch, but is not dynamically typed2
  • If you see articles or posts treating a trait like a type, it's probably from before the dyn in dyn Trait was required, so be aware and insert dyn in your head (the ambiguity does make such articles harder to understand)
  • Associated types (or other items) are part of the overall type of a dyn Trait and cannot be independently coerced3
  • If a trait doesn't meet your needs (e.g. because of an associated type you wish you could abstract over), sometimes you can define your own trait that does meet your needs and tie them together4
With a bunch of foot notes that you can hopefully put off thinking about until later.
  1. Lifetimes and types that are higher-ranked over lifetimes do have subtypes. And dyn Trait<'lifetime> is one such higher-ranked type. But the subtyping is between dyn Trait<'a> and dyn Trait<'b> for example, not dyn Trait<'a> and dyn DifferentTrait<'b>. (And not between associated types.)
  2. There is downcasting via Any, but it's limited ('static), type based and not trait based, somewhat cumbersome, doesn't allow for dynamic polymorphism (every variable still has a static type), etc. Generics or enums are usually a better fit. (Granted, generics won't let you put different base types into a Vec.) My advice is ignore Any until you already have a good feel for the more canonical patterns in Rust.
  3. But if they otherwise match types, lifetime subtyping may come into play.
  4. I gave one example before. Supertraits are a related approach. Note that a dyn Trait can be used to utilize its supertraits. If you find yourself wanting a &(dyn A + B), a new trait with supertraits is likely the answer5.
  5. (But note that you can have (dyn A + AutoTrait) where AutoTrait is any number of auto traits. You just need the indirection to combine non-auto-traits.)

...and there are other things I didn't even touch on. It's a wide-ranging topic that hits upon a lot of other areas of Rust, in addition to a whole lot of special behavior of its own.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.