RPIT Question for the 2024 edition

Let me clarify what I'm asking:

fn t1<'a, T>(data: Vec<T>) -> impl Iterator<Item = T> + 'a {
   data.into_iterator()
}

It should implicitly be T: 'a because we specified an associated type binding.

And if we don't specify an associated type binding, we are required to explicitly do a lifetime constrain the captured T in the hidden type:

fn t2<'a, T>(data: Vec<T>) -> impl Iterator + 'a where T: 'a {
   data.into_iterator()
}

Wouldn't this default make sense?

Let's just also clarify these things.

T: 'lifetime tells us two things.

  1. T cannot outlive 'lifetime.
  2. All references inside T must live at least as long as 'lifetime

impl Trait + 'lifetime tells us only one thing.

  1. impl Trait cannot outlive 'lifetime, i.e. the opaque type cannot outlive 'lifetime.

impl Trait + use<'lifetime> means the hidden type is allowed to use the 'lifetime. However, it doesn't imply that the hidden type must capture the lifetime unless it's actually needed. It also means that we only capture 'lifetime, so if we have another lifetime that the hidden type captures, it will not allow it.

Am I wrong?

Old post

My understanding about this change might not be fully informed, so feel free to correct me if I'm getting something wrong.

The new change will by default capture all in-scope type params + lifetimes.

Is there a reason why RPITs with associated type bindings (e.g. Item = T in impl Iterator<Item = T>), and a lifetime bound (e.g. + 'lifetime)--don't automatically also constrain associated type bindings with said lifetime?

For concrete (w/o generics) type with lifetime bounds (e.g. ConcreteType<T>: 'lifetime), all of the parameters of that type get the same lifetime implicitly. We can even use covariance to implicity do lifetime bounds (e.g PhantomData<T>: 'lifetime).

I understand that the difference is because RPITs are opaque types, so they have different semantics than concrete types. But that only makes sense if we don't specify any associated type bindings.

If we have specified them, wouldn't it make more sense that they also get implicit lifetime bounds?

Let me give an example of what I mean.

  1. -> impl Iterator<Item = T> + 'a
  2. -> impl Iterator + 'a

For me, when I see #1 I interpret it as if T has an implicit lifetime bound because of the explicit associated type binding (i.e Item = T), but that is not the case currently.

#2 would then work like it does now, it is an opaque type, so any outlive bound would just apply to the opaque type, nothing propagates down like it would do for concrete types.

This would work with the new Ăąse<> syntax.

EDIT: wait I think I misunderstood the question here whoops. Associated types in RPIT don't get implicitly bound because they don't need to be. There's nothing wrong with &'a &'static T. Input types get implicitly lifetime bound because that's more permissive to the caller by being more restrictive to the bound impl.

The underlying reason is that + 'a and + use<'a> mean different things. The difference is very subtle, because for a single covariant lifetime there isn't one.

To explain it simply:

  • + 'a means the object can validly live for at least 'a. You can use the object as long as any lifetime bound to it in this manner is still live.
  • + use<'a> means the object is live for at most 'a. You can use the object as long as all lifetimes captured by it in this manner are still live.

To explain by example: Every RPIT is + use<'static> (except that this would turn off the implicit capture). If you capture a lifetime (+ use<'a>) you can't be + 'static. e.g. &'a T works with + use<'outlives_a> but not with + 'outlives_a.

A different example: impl 'a means usable by T: 'a. + use<'a> means not usable outside of lifetime 'a.

For a more involved explanation, see the blog post about the RPIT changes.

2 Likes

EDIT: I've updated my post. Please take a look.

Significant and multiple edits[1] and "please reread from scratch" don't work well on this forum. Just write a new comment next time.

Your example doesn't really make sense (it introduces a lifetime pulled out of nowhere for no reason),[2] so it's hard to know why you think it does. Or why you think it's a 2024 edition specific thing.

The first bullet point is incorrect. If that's all some piece of code knows, it can't assume otherwise, but lifetime bounds on types aren't an upper limit (they're a lower limit). The second bullet point is true (indirectly).

+ 'lifetime in RPIT is an outlives bound on the opaque type. If that's all the caller knows (e.g. there's no other, longer lifetime bound), the caller can't assume it lives longer. It still means there are no references longer than 'lifetime in the opaque type.

You'd probably have to define your terms better to get a great answer. use<>-ing is often called capturing.

use<>-ing a lifetime means the opaque type is considered parameterized by the lifetime,[3] and that the lifetime may be used in the definition of the hidden type (but doesn't have to be). It's analogous to a GAT parameter.[4]

Variance can allow you to do things like have multiple lifetimes on multiple inputs, return all the inputs, and still only parameterize the opaque type by one lifetime.

If all of the parameters of the opaque type are known to outlive some lifetime 'x, the opaque type itself is known to outlive 'x.


  1. I currently see 7 on the OP and 5 on this one ↩︎

  2. and into_iterator isn't a method ↩︎

  3. if the lifetime differs, the opaque type differs, from the caller's perspective ↩︎

  4. Note that opaque types never normalize, unlike GATs. ↩︎

Yeah my bad, I will keep that in mind next time.

I know that it's not easy to understand at first glance, so I will try to restate it further down.

It seems that we have different interpretations of how to express a generic lifetime bound.

You mention that the first bullet point is incorrect because lifetime bounds on types aren't an upper limit but a lower limit, which is a valid observation. However, my point is more general (or based on a different premiss).

When I say "T cannot outlive 'lifetime," I'm referring to the fact that any references within T are constrained by 'lifetime. In other words, the validity of T depends on those references being valid for at least 'lifetime, which effectively prevents T from outliving that bound when it comes to the lifetimes of its references.

I'm saying, a T cannot outlive its references.

You are saying, T must live at least as long as its references.

They lead to different conclusions.

So I think we are basically saying the same thing, but with different axioms?

I agree that this doesn't imply that T can't live longer in other contexts, but in terms of the references within T, the lifetime bound does ensure that T cannot outlive 'lifetime.

I think there is some confusion over quantification, i.e. which cases are being considered, here. If we have a generic parameter <T: 'a>, then:

  • A valid concrete type to substitute for T is any type whose contained references all live for at least 'a.

    Therefore, in general, if T: 'longer_lifetime is true, then T: 'shorter_lifetime is also true. This is the “must live at least as long” idea.

  • A valid usage of the type parameter T, within the generic item, is one which does not assume that T lives longer than 'a.

    Therefore, if you have a T — in the sense that all you know is that it is a T — then you are not allowed to own or borrow a T for longer than 'a. This is the “cannot outlive” idea.

Both of these are true, but they are different perspectives on T; one is about the set of types that can be substituted for T, by the caller/instantiator of the generic code, and the other is about the things that can be done with T as a whole, by the callee/implementor, the generic code itself.

These work oppositely in the same way that a rule of “x must be less than y” has two opposite perspectives: if you are choosing x then you can make it smaller than required if you like, and if you are choosing y then you can make it bigger than required if you like.

3 Likes