Jon Gjengset Talk on `impl Trait` on Return Position Type and Lifetime Capturing

References / Context:

  1. Jon Gjengset Talk: https://www.youtube.com/watch?v=CWiz_RtA1Hw

  2. Slide of said Talk: https://docs.google.com/presentation/d/1U27Yr8MniRMUfxfPlwYt3wkYm6EtTWv42slYGRdgW1M/edit#slide=id.g282cb0d72f5_0_7

Question:

fn gen<’a>(t: &’a ()) -> impl Sized + ‘a {
    t
}

Jon discussed the above code snippet, gen is a function that takes in a t which is a reference that lives AT LEAST 'a and is of the unit type. It returns a concrete type that implements the trait Sized and lives AT LEAST 'a.

So far so good.

But Jon says that the problem is that the lifetime annotations mean that t lives for AT LEAST 'a which is NOT what the intent was; the intent was to annotate that t lives for AT MOST 'a.

Jon then gives the solution as follows:

trait Captures<U> {}
impl<T: ?Sized, U> Captures<U> for T {}
fn gen<’a>(t: &’a ())
  -> impl Sized + Captures<&‘a ()> {
    t
}

I don't see how by adding Captures<&‘a ()>, it tells the compiler that now the gen function takes in a t that lives AT MOST 'a.

2 Likes

When a type has a lifetime annotation, then this means that values of that type live for at most that lifetime. The captures trait essentially just adds a lifetime annotation, hence it does what you want.

The syntax + 'a means "this type has no lifetime annotations shorter than 'a". That is, the type cannot have an "at most" restriction shorter than 'a, so it lives for at least 'a.

6 Likes

This is exactly the point of https://github.com/rust-lang/rfcs/blob/master/text/3498-lifetime-capture-rules-2024.md which you should know first.

In short, -> impl Trait + 'a is considered to be incorrect in the lifetime constraint:

  • -> impl Trait + 'a, the so-called 'outlives trick', means the opaque return type outlives 'a
  • but you actually should mean 'a outlives the opaque return type, which needs the 'capture trick' -> impl Sized + Captures<&'a ()>

...
Consider what impl Sized + 'a means. We're returning an opaque type and promising that it outlives any lifetime 'a.

This isn't actually what we want to promise. We want to promise that the opaque type captures some lifetime 'a, and consequently, that for the opaque type to outlive some other lifetime, 'a must outlive that other lifetime. If we could say in Rust that a lifetime must outlive a type, we would say that the 'a lifetime must outlive the returned opaque type.

That is, the promise we're making is the wrong way around.

It works anyway in this specific case only because the lifetime of the returned opaque type is exactly equal to the lifetime 'a. Because equality is symmetric, the fact that our promise is the wrong way around doesn't matter.
...

5 Likes

There was some more recent discussion (and citations) in this thread if you want to dig deeper.

I love your reply because it is concise, answers the question and straight to the point.

I see that there is some contravariance/covariance stuff here pertaining to where the annotation is placed, on the struct vs. in the opaque return type. Or am I mistaken?

The -> impl Trait lifetimes act like lifetimes through a trait projection (Trait::<'a>::Ty), i.e. they are invariant.

3 Likes

Sir, thank you for your response; would you mind pointing me to more resources on 'trait projection'?

Thank you.

I'm afraid I don't have a proper citation; I don't believe an official one (which directly discusses this) exists.

RFC 1214 has a lot of discussion about projections, but invariance is barely mentioned.

However, if you think about a an unnormalized projection Trait::<'a>:::Ty, it can't be anything but invariant in this form, because these are all valid implementations.

impl<'a> Trait<'a> for Covariant {
    type Ty = &'a str;
}
impl<'a> Trait<'a> for Contravariant {
    type Ty = fn(&'a str);
}
impl<'a> Trait<'a> for Invariant {
    type Ty = &'static mut &'a String;
}
impl<'a> Trait<'a> for Bivariant {
    type Ty = String;
}

If you can normalize it for a specific implementation, you can "recover" the underlying variance. Now the missing piece with RPIT or TAIT is, does the compiler (internally) normalize these opaque types? Again I don't have a citation, but as far as my experiments have gone, it seems like it does not.[1]

Is it feasible in the future and, if so, would it be entertained by the relevant teams? I also don't know.


  1. Also discussed in the other thread. ↩︎

2 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.