Compiler limitations for associated types

Hi!

I'm wondering if it's possible in rust to have a function return some implementation of a trait that uses associated types in general.

Here's an example set up:

fn main() {
    use std::marker::PhantomData;

    trait ATrait {}
    impl ATrait for usize{}

    trait BTrait<T, L> where T: Into<usize>, L: ATrait {}
    struct BImpl<T, L> {
        phantom1: PhantomData<T>,
        phantom2: PhantomData<L>,
    }
    impl<T, L> BTrait<T, L> for BImpl<T, L> where T: Into<usize>, L: ATrait {}

    trait MyTrait {
        type A: ATrait;
        type B: BTrait<usize, Self::A>;
    }

    struct Impl1{}
    impl MyTrait for Impl1 {
        type A = usize;
        type B = BImpl<usize, usize>;
    }

    struct Impl2{}
    impl MyTrait for Impl2 {
        type A = usize;
        type B = BImpl<usize, usize>;
    }
}

The goal here would be to have a function say new_mytrait that returns an arbitrary implementation of MyTrait to the caller - so sometimes Impl1 sometimes Impl2.

Ideally this would be something like:

    // A function that explicitly returns e.g a boxed object is also welcome.
    fn new_mytrait<M: MyTrait>() -> M {
        if true {
          Impl1{}
        } else {
          Impl2{}
        }
    }

    let m = new_mytrait();
  • Is such a function possible in rust? What does it look like?

Somethings I noticed about this:

  • Rust really really wants you to spell out values for all the associated types in traits that have them. As an example:
trait Foo {
  type A;
}
struct Bar {}
impl Foo for Bar {
  type A = usize;
}
let b: Box<dyn Foo> = Box::new(Bar{}); // Does not compile
let b: Box<dyn Foo<_>> = Box::new(Bar{}); // Does not compile
let b: Box<dyn Foo<A=usize>> = Box::new(Bar{}); // OK

Its not clear to me besides compiler limitations, why the compiler doesn't infer the type of b - its clear from the trait implementation for Bar?
The issue here is that in a trait with multiple associated types, each in turn traits with associated types and so on, it won't be practical to spell out the type explicitly.

  • But back to another issue with the MyTrait example is that rust says it doesn't know how to create a trait object for it to begin with, because the trait self-references its associated types.
   |
14 |     trait MyTrait {
   |           ------- this trait cannot be made into an object...
15 |         type A: ATrait;
16 |         type B: BTrait<usize, Self::A>;
   |                 ---------------------- ...because it uses `Self` as a type parameter in this
...
31 |             let b: Box<dyn MyTrait<A= usize, B= BImpl<usize, usize>>> = Box::new(Impl1{});
   |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `main::MyTrait` cannot be made into an object

Are these compiler quirks or maybe I'm missing something with these behaviors?

You could do something like this

enum Either<A, B> {
    Left(A),
    Right(B),
}

impl<L: MyTrait, R: MyTrait<A = L::A, B = L::B>> MyTrait for Either<L, R> {
    type A = L::A;
    type B = L::B;
}

fn new_mytrait() -> impl MyTrait<A = usize, B = BImpl<usize, usize>> {
    if true {
      Either::Left(Impl1{})
    } else {
      Either::Left(Impl2{})
    }
}

Thanks for the response!
Here, Either seems to assume that both implementations have identical values for the associated type parameters? which is only coincidental (meaning my lazy copy pasting ^^) in the example. For example it would no longer capture the problem if e.g Impl1 was instead

impl MyTrait for Impl1 {
  type A = String; // where String satisfied ATrait ofc
  type B = BImpl<usize, String>;
}

fn new_mytrait() -> impl MyTrait<A = usize, B = BImpl<usize, usize>> {
I'm guessing the return type would need to, in some way, express that the returned value can have any combination of types A=impl Atrait and B=impl BTrait that satisfy MyTrait? since there could be any number of implementations of MyTrait and each implementation has different values for A and B

let b: Box<dyn Foo<A=_>> = Box::new(Bar{}); seems to work.

I guess this “limitation” is for preventing confusion where one could think that different dyn Foos could be the same type when they aren’t.




I would personally categorize this more on the compiler quirk side, as I don’t see any reason why it shouldn’t be allowed. And even if there was such a reason, the error message is not particularly good either: Self is not a parameter of BTrait here.

This is required if you want to return either one of them. You can't abstract over trait implementations if they have different associated types.

Returning different types depending on a run-time condition would require dependent types which are not available in Rust. A usual workaround for this specific problem is to replace returning the value with calling a generic function on it:

fn handle<T: MyTrait>(value: T) {
    //...
}


fn new_mytrait() {
    if true {
        handle(Impl1{})
    } else {
        handle(Impl2{})
    }
}
1 Like

Thanks all for the replies! They've been very informative

Returning different types depending on a run-time condition

I think this highlights the first problem better than my example did - I can do this for simple traits but I guess that this isn't supported for at least traits with associated types.

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.