GAT and associated types with lifetime

It is such a relief that GAT implementation has finally merged into stable branch of rust a few days ago. And before my question, I would express my appreciation to all members contributing to this giant leap. However, I have a little problem about GAT and associated types with lifetime.

One benefit of GAT is that we can use struct with reference field (i.e. struct with lifetime parameter) as an associated type. For example:

struct StructWithLifetimeParameter<'a> {
    field_of_reference: &'a Foo,
}

trait GatTrait {
    type CanBeGeneric<'a>;
}

impl GatTrait for SomeOtherStruct {
    type CanBeGeneric<'a> = StructWithLifetimeParameter<'a>;
}

However, this also means that using struct with lifetime parameter as an associated type is intrusive. Unless the author of a crate uses GAT to rewrite the trait, the user of such crate will never use struct with lifetime parameter as an associated type.

The fact is that, in most cases, whether an associated type is a struct with or without lifetime parameter is not semantically relevant to the crate author. For example, an author may provide a trait with associated type Context, and the user may use a reference in the corresponding struct to avoid coping large memories across the application. But whether Context is generic over lifetime or not does not change anything in the author's side. While if the author does not provide a GAT version, the user cannot use this graceful feature, but to use RefCell, Rc or something else to achieve this.

So here is my wrong conclusion: If whether an associated type generic over lifetime or not does not change anything in author's side, then the author should use GAT, since users may use struct with reference field as that associated type.

I don't think such conclusion is appropriate, since GAT is very complicated and should not be abused. But I don't know what is the right way to solve this dilemma.

Nice to hear it landed!

I could be mistaken, but it sounds like you're describing the case:

impl<'a> RegularTrait for SomeOtherStruct {
    type NotGeneric = StructWithLifetimeParameter<'a>;
}

GATs are exactly for the cases where the user of the trait very much cares about the generic parameter.

@simonbuchan Thank you for your reply. Your code cannot compile: this playground. And as far as I know, GAT is the only way to use struct with lifetime as associated types.

Serves me right for posting on my phone! I was thinking of:

impl<'a> RegularTrait for SomeOtherStructWithLifetimeParameter<'a> {
    type NotGeneric = StructWithLifetimeParameter<'a>;
}

impl<'a> GenericTrait<'a> for SomeOtherStruct {
    type NotGeneric = StructWithLifetimeParameter<'a>;
}

I believe the restriction is because otherwise the use-site can't know what lifetime to use, so you will generally have one of the above cases anyway.

To add a lifetime generic on the trait GenericTrait itself is also intrusive, so I would talk about the first one.

The first one is indeed a solution to this problem, but I think there are some situations where it is meaningful to only add lifetime generic to associated type but not the implemented struct itself, and in those situations, adding a phantom lifetime field to the struct is not very graceful.

For example, the associated type is Context, and the trait will generate a struct from this context (just compute, the final struct will not borrow the context):

trait Computable {
    type Context;
    fn compute(context: Self::Context) -> Self;
}

struct MyStruct;

struct MyContext<'a> {
    ref_to_a_huge_struct: &'a HugeStruct,
}

// How to implement Computable for MyStruct?

This example may be work around by using another design pattern, but I do think there are still some situations where adding lifetime generic to struct itself is not that meaningful.

And I do think that, if this is the right way, official rust docs should noted about this when formally introducing GAT.

There's a flaw in your logic, which is that impl lifetimes can be attached to the type as well as the trait. Just because someone can implement Computable<Context = MyContext<'some_lifetime>> does not mean it can implement Computable<Context = MyContext<'any_other_lifetime>>.

In other words, it's valid to write

impl<'a> Computable for &'a MyStruct {
    type Context = MyContext<'a>;
    fn compute(context: Self::Context) -> Self {
        // presumably retrieve some kind of interned `MyStruct` by reference?
    }
}

This is meaningful, allowed by the definition of Computable, and is not compatible with making Context a GAT, because the lifetimes of Self and Context are connected.

This is true, but the example doesn't support the conclusion that trait authors should just sprinkle GATs around like candy. If the trait author wants to allow implementations like the above, a GAT is wrong. If they want to allow implementations like the following:

impl Computable2 for MyStruct {
    // ignore the type for now
    fn compute<'a>(context: &'a MyContext) -> Self;
}

possibly along with other kinds of ways of referencing MyContext, then the most logical way to express that is with a generic parameter and not an associated type,

trait Computable2<Context> {
    fn compute(context: Context) -> Self;
}

which makes the lifetime parameter invisible to the trait author, because it is irrelevant, and gives the implementer free rein to use &MyContext or Rc<MyContext> or just MyContext, even with the same MyContext, if they so desire.

Traits mean things. Replacing a non-generic associated type with a generic one changes the meaning of the trait, and should not be done lightly. It does not just mean "hey, this thing might have a lifetime parameter".

2 Likes