What's the difference between a trait's generic type and a generic associated type?
Asked Answered
P

2

11

This question is asked before generic associated types are available in Rust, although they are proposed and developed.

My understanding is that trait generics and associated types differ in the number of types which they can bind to a struct.

Generics can bind any number of types:

struct Struct;

trait Generic<G> {
    fn generic(&self, generic: G);
}

impl<G> Generic<G> for Struct {
    fn generic(&self, _: G) {}
}

fn main() {
    Struct.generic(1);
    Struct.generic("a");
}

Associated types bind exactly 1 type:

struct Struct;

trait Associated {
    type Associated;

    fn associated(&self, associated: Self::Associated);
}

impl Associated for Struct {
    type Associated = u32;

    fn associated(&self, _: Self::Associated) {}
}

fn main() {
    Struct.associated(1);
    // Struct.associated("a"); // `expected u32, found reference`
}

Generic associated types are a mix of these two. They bind to a type exactly 1 associated generator, which in turn can associate any number of types. Then what is the difference between Generic from the previous example and this generic associated type?

struct Struct;

trait GenericAssociated {
    type GenericAssociated;

    fn associated(&self, associated: Self::GenericAssociated);
}

impl<G> GenericAssociated for Struct {
    type GenericAssociated = G;

    fn associated(&self, _: Self::GenericAssociated) {}
}
Pother answered 20/2, 2019 at 17:1 Comment(0)
T
14

Let's take a look at your last example again (shortened by me):

trait GenericAssociated {
    type GenericAssociated;
}

impl<G> GenericAssociated for Struct {
    type GenericAssociated = G;
}

This does not feature generic associated types! You are just having a generic type on your impl block which you assign to the associated type. Mh, ok, I can see where the confusion comes from.

Your example errors with "the type parameter G is not constrained by the impl trait, self type, or predicates". This won't change when GATs are implemented, because, again, this has nothing to do with GATs.

Using GATs in your example could look like this:

trait Associated {
    type Associated<T>; // <-- note the `<T>`! The type itself is 
                        //     generic over another type!

    // Here we can use our GAT with different concrete types 
    fn user_choosen<X>(&self, v: X) -> Self::Associated<X>;
    fn fixed(&self, b: bool) -> Self::Associated<bool>;
}

impl Associated for Struct {
    // When assigning a type, we can use that generic parameter `T`. So in fact,
    // we are only assigning a type constructor.
    type Associated<T> = Option<T>;

    fn user_choosen<X>(&self, v: X) -> Self::Associated<X> {
        Some(x)
    }
    fn fixed(&self, b: bool) -> Self::Associated<bool> {
        Some(b)
    }
}

fn main() {
    Struct.user_choosen(1);    // results in `Option<i32>`
    Struct.user_choosen("a");  // results in `Option<&str>`
    Struct.fixed(true);        // results in `Option<bool>`
    Struct.fixed(1);           // error
}

But to answer you main question:

What's the difference between a trait's generic type and a generic associated type?

In short: they allow to delay the application of the concrete type (or lifetime) which makes the whole type system more powerful.

There are many motivational examples in the RFC, most notably the streaming iterator and the pointer family example. Let's quickly see why the streaming iterator cannot be implemented with generics on the trait.

The GAT version of the streaming iterator looks like this:

trait Iterator {
    type Item<'a>;
    fn next(&self) -> Option<Self::Item<'_>>;
}

In current Rust, we could put the lifetime parameter on the trait instead of the associated type:

trait Iterator<'a> {
    type Item;
    fn next(&'a self) -> Option<Self::Item>;
}

So far so good: all iterators can implement this trait as before. But what if we want to use it?

fn count<I: Iterator<'???>>(it: I) -> usize {
    let mut count = 0;
    while let Some(_) = it.next() {
        count += 1;
    }
    count
}

What lifetime are we supposed to annotate? Apart from annotating the 'static lifetime, we have two choices:

  • fn count<'a, I: Iterator<'a>>(it: I): this won't work because generic types of a function are choosen by the caller. But the it (which will become self in the next call) lives in our stack frame. This means that the lifetime of it is not known to the caller. Thus we get a compiler (Playground). This is not an option.
  • fn count<I: for<'a> Iterator<'a>>(it: I) (using HRTBs): this seems to work, but it has subtle problems. Now we require I to implement Iterator for any lifetime 'a. This is not a problem with many iterators, but some iterators return items that don't life forever and thus they cannot implement Iterator for any lifetime -- just lifetimes shorter than their item. Using these higher ranked trait bounds often leads to secret 'static bounds which are very restricting. So this also doesn't always work.

As you can see: we cannot properly write down the bound of I. And actually, we don't even want to mention the lifetime in the count function's signature! It shouldn't be necessary. And that's exactly what GATs allow us to do (among other things). With GATs we could write:

fn count<I: Iterator>(it: I) { ... }

And it would work. Because the "application of a concrete lifetime" only happens when we call next.

If you are interested in even more information, you can take a look at my blog post “ Solving the Generalized Streaming Iterator Problem without GATs” where I try using generic types on traits to work around the lack of GATs. And (spoiler): it usually doesn't work.

Transvestite answered 21/2, 2019 at 9:10 Comment(8)
about the second point that you mentioned fn count<I: for<'a> Iterator<'a>>(it: I) — Could you please provide an example on how it would fail for being restrictive (i.e secret 'static bound)?Southwestwards
@cotigao I describe the limitation in more detail in this blog post (link has an anchor to the correct section). In short: if you want to have your item be a reference to a generic with the iterator lifetime (i.e. impl<'s, T> Iterator<'s> for ... { type Item = &'s T; }), you have to bound T: 's. Otherwise Rust won't let you write that reference type. And the bounds for<'a> I: Iterator<'a> plus T: 's lead to T: 'static. I hope that helps!Transvestite
@cotigao Oh and you can see a concrete example in this playground (also linked in the blog post). At the very bottom we cannot pass the WindowMut iterator to count because T is not 'static.Transvestite
Thanks for the post! One question: Quoting from your article; " for<'s> T: 's which is equivalent to T:'static " ⸺ Is it equal because the iterator borrows T indirectly (i.e via the slice that itself borrows the elements with a lifetime of 'a) ?Southwestwards
@cotigao I'm not sure I understand your question. for<'s> B means "for all possible lifetimes, the bound B has to hold". This statement also includes "B has to hold for 'static", right? Because that is "a possible lifetime". And since 'static in a sense "includes" all other lifetimes, the two bounds are equivalent.Transvestite
I have tried to come up with a minimal version. What is the difference between test and test2 ? The former expects that the concrete type has to have only 'static references, where as the latter says that the trait Trait<'s> is implemented for all possible (i.e 's) lifetimes and accordingly anyone is picked ? I am a bit confused, as I thought of them the same.Southwestwards
@cotigao That's a good question. I'd be great if you could ask a "proper" (i.e. a post) question here on SO, as this comment section is likely too small for a full explanation. But to be clear: I'm not sure of the explanation right now. HRTBs are not trivial.Transvestite
Thanks! I have posted a separate questionSouthwestwards
D
10

What's the difference?

Generic Associate Types (GATs) are associated types which are themselves generic. The RFC starts with a motivating example, emphasis mine:

Consider the following trait as a representative motivating example:

trait StreamingIterator {
    type Item<'a>;
    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

This trait is very useful - it allows for a kind of Iterator which yields values which have a lifetime tied to the lifetime of the reference passed to next. A particular obvious use case for this trait would be an iterator over a vector which yields overlapping, mutable subslices with each iteration. Using the standard Iterator interface, such an implementation would be invalid, because each slice would be required to exist for as long as the iterator, rather than for as long as the borrow initiated by next.

This trait cannot be expressed in Rust as it exists today, because it depends on a sort of higher-kinded polymorphism. This RFC would extend Rust to include that specific form of higher-kinded polymorphism, which is refered to here as associated type constructors. This feature has a number of applications, but the primary application is along the same lines as the StreamingIterator trait: defining traits which yield types which have a lifetime tied to the local borrowing of the receiver type.

Note how the associated type Item has a generic lifetime 'a. Most examples in the RFC use lifetimes, but there's also an example using a generic type:

trait PointerFamily {
    type Pointer<T>: Deref<Target = T>;
    fn new<T>(value: T) -> Self::Pointer<T>;
}

Note how the associated type Pointer has a generic type T.

Your specific example

what is the difference between Generic from the previous example and this generic associated type

There may be none, and the existence of GATs would not help your case, which does not seem to require an associated type which is itself generic.

Daisydaitzman answered 20/2, 2019 at 17:30 Comment(7)
This makes "regular" generics just a syntax sugar over generic associated type, doesn't it? I'm slightly surprised that Rust is trying to introduce a second way to express the same thing, it's usually avoided wherever possible.Pother
@Pother I'm not sure I follow. Is struct Foo "syntax sugar" over struct Foo<T>?Daisydaitzman
trait Foo<T> {} is syntax sugar over trait Foo { type T; } when GATs are implemented in the final formPother
That's... not quite accurate. Associated types allow the trait implementor to choose a type, which the trait then operates over. Generic types allow the callee to choose a type, which the trait operates over. The distinction about who chooses, and when, is core to the difference here. See the Q&A here for moreBelda
GAT's allow trait author to require implementors to choose a generic type which meets certain constraints - this solves a number of issues that have come up as limitations of the current system - two examples are discussed in the answer.Belda
But if trait author requires implementor to choose generic type which meets no constraints (e.g. trait Foo { type T; }) then implementor is free to allow callee to choose a type (e.g. impl<T> Foo for Bar { type T = T; }), which behaves same as regular generics (e.g. trait Foo<T> {}). It makes regular generics an edge case of GATs. Is this reasoning correct?Pother
@Pother impl<T> Foo for Bar { ... } is always incorrect and there is nothing, GAT-related or otherwise, that you can put in the ... to make it compile. Lukas's answer explains why. In GATs the impl is not generic, the associated type itself is.Pension

© 2022 - 2024 — McMap. All rights reserved.