Proposal: `'<T>` lifetime

What it is

For any type T, '<T> (bikeshedabble ofc) denotes the longest lifetime 'a for which T: 'a holds.

Why it’s useful

Eliminating early-bound lifetime parameters

The signature of Box::leak() contains an early-bound lifetime parameter:

impl<T: ?Sized> Box<T> {
    fn leak<'a>(b: Box<T>) -> &'a mut T { … }
}

This feature would allow eliminating that parameter:

impl<T: ?Sized> Box<T> {
    fn leak(b: Box<T>) -> &'<T> mut T { … }
}

Self-referential lifetimes

Consider the following self-referential data structure (example taken from Unsafe binder types - HackMD):

struct DataAndView<T> {
    x: Box<[T]>,
    element: &'static T,
    //~^ ERROR the parameter type `T` may not live long enough
}

This unnecessarily imposes a 'static bound on T. With this feature, we can instead write:

struct DataAndView<T> {
    x: Box<[T]>,
    element: &'<T> T,
}

Avoiding this bound.

4 Likes

Not quite; a function like fn f<T>() -> T with an output generic type can still get an effectively early-bound lifetime by using '<T>. This might make the difference between early-bound and late-bound clear enough to make their differing behavior more intuitive, but it can't eliminate the concept entirely.

Well, not quite, since the function already exists and can be referred to with the parameter explicitly provided.

This is better handled by unsafe binder types, e.g. element: unsafe<'a> &'a T.


Thus I don't really see sufficient motivation. Early-bound "output" generic lifetimes aren't any stranger to users than "output" generic types are, ime, and adding this can't remove the compiler complexity around dealing with early-bound vs late-bound, since it'll always be possible to declare early-bound lifetimes in existing editions.

Yes, this is specifically intended as an alternative to that; I link to the unsafe binders HackMD in the post.

Yes, this is a limitation, but can be fixed over an edition.

Well, yes, that is the point, the lifetime can now be derived from the type, so there is now only one early-bound parameter instead of two.


Extension

We could extend this feature to allow specifying an list of types, such that the resultant lifetime is outlived by all types in the list:

fn leak<T, U>(b: Box<(T, U)>) -> &'<T, U> mut (T, U) { … }

Lifetimes could also be permitted:

fn leak<'a, T>(b: Box<(&'a i32, T)>) -> &'<'a, T> mut (&'a i32, T) { … }

'<(P1, P2, ..)> would have been a sensible default dyn lifetime for those cases where the default for dyn Trait<P1, P2, ..> is currently 'static, IMO.

Which brings us to a use case for structs that isn't tied to self-referencial structs:

struct DoNotWantStaticRequirementButPreferNotHavingLifetimeParam<T> {
    bx: Box<dyn Trait<T> + '<T>>,
}

Introducing constrained lifetimes would introduce bivariant lifetimes in some sense. Whatever that's worth.

6 Likes

I don't think I can actually give a meaningful response to this proposal without doing the work of writing a full feature description and coming up with the edge cases myself. I'm pretty sure due to the nature of it taking lifetimes from a type you'll just run into conceptual cycles when other bounds on the type are involvee. That's even without any cycles in the type. Or you just bail and turn all variances in the most pessimistic variant, making the feature likely useless in the presence of mutable references

So at present I see this as a desire for a feature, not a proposal. It'll definitely need some explanation of why unsafe binders and late bound vars should be merged, not just stating that it could. Also some variance examples or usage examples would be helpful, as declarations by themselves do not explain semantics.

I don’t think it’s that complicated? '<whatever> is covariant in whatever. I don’t understand what else you think is missing?

IMO, a big unstated benefit of unsafe binder types is that they very clearly show that the lifetimes they bind are unknown to the compiler and that one needs to proceed with caution when using them. They even require unsafe to use the inner type. This proposal does not do that.

1 Like

I agree that requiring unsafe is important. But https://github.com/rust-lang/rfcs/pull/3458 addresses the issue, and I’m not convinced that unsafe binders add much extra value on top.

struct T<'lt> {
    inner: Rc<T<'<Self>>>,
}

Would there be semantic meaning to this? '<T> = 'static?

That’s an an error, as 'lt is unused. (struct Foo<'a>(*const Self); also doesn’t work)

That was imprecise of me. I was getting at the explanations being very unclear about how lifetime solving would work algorithmically if we get to mention these. Let's restate it with 'lt being covered for concreteness sake:

struct T<'lt> {
    inner: Rc<T<'<Self>>>,
    covers_lt: PhantomData<fn() -> &'lt ()>,
}

Note that any arbitrary assignment of '<T<'lt>> seem correct (as long as 'lt:'<T<'lt>>? maybe? or maybe not?). Are we defining that the lifetime should be the maximum element in the lattice of solutions or the minimum?

And now consider the another case:

struct Foo<'lt> {
    my_head_hurts: &'Foo<'lt> dyn FnOnce() -> &'Foo<'lt> &'lt (),
}

The lifetime for Foo is now at most 'lt for the return value to be a well-formed type. But again, any arbitrary assignment shorter than 'lt is a correct solution, it's just incompatible with the alternate shorter solutions. So which one should the compiler choose?

Of course if we combine it just right we have unique solutions, only how do we find that uniqueness:

struct Foo<'lt> {
    my_head_hurts: &'lt dyn FnOnce() -> &'Foo<'lt> &'lt (),
}

The OP’s first sentence answers this question: we choose the longest lifetime fulfilling the requirements. In this case, that’s 'lt, so '<Self> is 'lt. (In general, referencing Self in a field of a struct will never shorten the struct’s lifetime. This is true today, and the same rules apply to this feature.)

'<Foo<'lt>> is also 'lt in your last two examples, for the same reasons.

The sentence already supposes that there is a well-defined unique lifetime 'a for every type T that is the longest possible choice. I'm asking to demonstrate that this is indeed the well-formed. As demonstrated the 'longest possible lifetime' is also a function of the lifetimes we assigned for other types T: '<T>.

I already fail to see the obviousness of how that is a monotonic lattice problem that can simply be hill-climbed to a maximum. In contrast to borrow checking, the lifetimes in the type system are supposed to be constants (as per said definition; we do not get a lower bound, we get the lifetime). In borrow checking we can stop when there is a feasible solution, we do not find the optimal one; also all variables in every instance are fresh which makes the problem local. I don't see how that problem is local within the whole type system where new definitions are added at later points. How and where do we diagnose errors? I think it comes down to pointing out at which points the lifetimes '<T> are treated as free variables and where they are not. Another well-formedness problem:

struct Foo<'a, 'c> { … };
struct Bar<'b, 'd> { … };

struct Explain<'a, 'b> {
    inner: &'<Foo<'a, 'b>> &'<Bar<'a, 'b>> ();
}

This requires '<Bar<'a, 'b>>: '<Foo<'a, 'b>>. Do we have to name where '<Bar<'a, 'b>>: '<Foo<'a, b>> as a bound on the type Explain even though it looks like a trivial bound (either true or false if we treat those as lifetimes constants)? If we do not, does it introduce implied bound? Of what form? For instance we may find: '<Bar<'a, 'b>> <= min('a, 'b) from the definitionBar(&'a(), &'b()) and '<Foo<'a, 'b>> <= 'a for another reason. Does this introduce an implied bound 'b = 'a by the well-formedness requirement? If we had to write one manually it may make the problem more locally solvable.

I'm asking for an algorithmic description since: How are programmers supposed to reason about code written with this feature, it seems like it requires internal analysis of types to use those lifetimes.

In this example, '<Foo<'a, 'b>>, '<Bar<'a, 'b>>, and '<Explain<'a, 'b>> are all equivalent to '<'a, 'b> (using the extension I describe in this reply). More generally, for any type T, '<T> is the intersection of all lifetimes T mentions. (Again, this follows from the definition in OP and the current rules of Rust.)

This example actually exposes a weird quirk of Rust. Let’s simplify it to:

struct Explain<'a, 'b> {
    inner: &'<'a, 'b> ();
}

'<'a, 'b> is uniquely determined by 'a and 'b. However, the reverse is not true; given only the lifetime of '<'a, 'b>, you cannot infer what 'a and 'b are.

However, situations like this can already occur in today’s Rust. The compiler makes an angelic choice in such cases:

trait Gatt {
    type Gat<'a>
    where
        Self: 'a;
}

impl Gatt for () {
    type Gat<'a> = ();
}

#[derive(Clone, Copy)]
struct Foo<'lt>(<() as Gatt>::Gat<'lt>);

fn requires_static(_: Foo<'static>) {}

fn requires_not_static<'a>(_: Foo<'a>, _: &'a ()) {}

fn main() {
    let foo = Foo(());

    // Angelic choice:
    // commenting out either one of these two calls allows the program to compile
    requires_static(foo);
    requires_not_static(foo, &foo.0);
}

Another use case is making more function pointer types possible. Currently, it’s impossible to turn this function into a function pointer without loss of generality:

fn leak_tuple<'a, 'b, 'c>(a: &'a mut (), b: &'b mut ()) -> &'c mut (&'a mut (), &'b mut ()) {
    Box::leak(Box::new((a, b)))
}

With this proposal, you could instead write the function as:

fn leak_tuple<'a, 'b>(a: &'a mut (), b: &'b mut ()) -> &'<'a, 'b> mut (&'a mut (), &'b mut ()) {
    Box::leak(Box::new((a, b)))
}

Which could become:

type FnPtr = for<'a, 'b> fn(&'a mut (), &'b mut ()) -> &'<'a, 'b> mut (&'a mut (), &'b mut ());
1 Like

I just realized: this feature could entirely replace the new “precise capturing” syntax (use<…>). Any + use<…> would become + '<…>.

2 Likes