Lets remove explicit lifetimes


#21

I understand the value of lifetimes, and explicitly naming them when there are many of them mixed together, but I really dislike the bureaucracy of the simplest, most basic case:

struct Foo { bar: &usize }

AFAIK in this specific case with only one reference there’s no ambiguity. The lifetime could be elided.

I think Rust would be much more user-friendly if it started requiring explicit lifetimes only where necessary (when you start using multiple references, nested structs with references, etc.). That would save experienced users some typing, but most importantly would lower learning curve for novice users.


#22

And here’s another nitpicky case where Rust wants me to write out redundant information:

struct Foo<'a, T> {
   bar: &'a T,
}

Rust knows T must live longer than 'a, there’s no other option, but it still wants me to write the obvious. Its yet another bit of syntax to learn and remember.


#23

Sure there is, because this is what the compiler bases it’s reasoning on:

struct Foo { ... }

That’s where the interface boundary is, so that’s where the information needs to go. I’m sympathetic to wanting to simplify this, but I see no way of doing this without requiring people to read and understand the complete, transitive type definition in order to work out what the top-level interface is. Don’t forget that lifetimes are not limited to references. One of Rust’s great strengths is having this sort of information in the interface, rather than implied by the implementation.

Edit: to clarify, it’s a great strength in so small part because both the compiler and programmer are working from the same, directly stated information. Hidden state may be fine for software, but it’s a bastard on the human brain.


#24

I’m not saying struct Foo should mean the interface is for lifetime-less struct. I’m saying when I type struct Foo with a reference inside, compiler should understand its obviously Foo<'a> and not have me type the 5 characters when it can unambguously infer it from the struct’s definition.

The same way I type struct Foo {i: usize}, but the interface is actually something like #[size(8), align(8)] struct Foo. Rust sees the struct body and computes these properties instead of requiring me to type them, and I don’t get errors like “struct alignment not specified” or “alignment 4 specified, but usize requires alignment 8”.


#25

In struct Foo {bar: &Bar} it is not hidden that it contains a reference. It is stated explicitly. The problem is Rust wants it stated twice.

& already states that the struct contains a reference, and states that the reference outlives the struct.


#26

How far do you have to dig to know the lifetime of Foo? What if it’s:

struct Foo { bar: Bar }
struct Bar { baz: Baz }
struct Baz { iter: std::slice::Iter<i32> }

// oops, go look at the standard library sources, slice.rs:
// (but I altered even this to not show explicit lifetimes)
pub struct Iter<T> {
    ptr: *const T,
    end: *const T,
    _marker: marker::PhantomData<&T>,
}

// ok, go find marker.rs...
pub struct PhantomData<T:?Sized>;

Three indirections to dig to PhantomData<&T>, and if you don’t know about that special type, you might still be confused whether the lifetime actually holds here or not.

Suppose there were also a Vec in Foo, which you probably know today doesn’t have a lifetime. But to really know this, you’d have to go look. You’d see it has a RawVec, which has a Unique, which has a NonZero and PhantomData

I hope this becomes clear to you how complex it may get if we don’t state lifetimes explicitly.


#27

I noted in my post, twice, that I’m talking only about the most basic case. One struct, one reference, no nested structs with references.

Also, Rust already is not always explicit about lifetimes:

struct Foo<T> {
    bar: T
}

It’s valid to have Foo<&Bar>, and this is used all the time everywhere.


#28

Ok, take just this:

struct Foo<'a> { bar: Bar<'a> }
struct Bar<'a> { baz: Baz<'a> }
struct Baz { x: &i32 }

Is that how you would have it, so only Baz can omit the lifetime?


#29

Yes, that’d be good. In all cases existence of references is still visible, but the basic case doesn’t require extra annotations.
I see it as similar to lifetime elision in functions. You don’t need to write fn foo<'a>(&'a self), so simple things remain easy and with less syntax noise.


#30

Note that lifetime elision in functions works for any type, not just references. e.g. fn foo(f: Foo) {} is fine for my Foo<'a> above. That’s not too bad, because you only have to look at the type signature of Foo to figure out if this function actually has a lifetime. With your proposal, you’d also have to look inside Foo for references.

Plus it would be inconsistent if functions can elide any lifetime while structs can only elide lifetimes of direct references. But if you generalize elision in structs the same way, we’re back to chasing struct definitions.


#31

“chasing references” is just not a problem I have. As soon as I use the type wrong, Rust will tell me all about it. I never even look at struct definitions, because function return types is pretty much the only place where it matters for me (and lifetimes are not required there already).

But I do have a problem with noisy syntax, incomplete and inconsistent inference, and having to type obscure and redundant syntax, and explosion of complexity as soon as I use reference in traits.

BTW: inference in source code doesn’t stop Rustdoc from knowing it, so the struct can still be clearly documented to contain references.


#32

In general I think its very unlikely we will require no annotation, but I would agree with you that this feels like a lot of boilerplate annotations. I think we’d be open to a syntax which means ‘every lifetime in this type is the same’, though what syntax exactly is unclear. So you could write:

struct Foo<'_, T> { 
    bar: &T,
}

struct Baz<'_>(Foo<u32>);

Or possibly:

struct Foo<&, T> {
    bar : &T,
}

struct Baz<&>(Foo<&, u32>);

The fundamental idea is that you can see that the type is parametric over a lifetime, but you don’t have to go through and add the annotation of the name of that lifetime to every reference within it, since there’s only going to be one lifetime.


#33

I like that first option, as long as you could use any lifetime name instead of the _ and still have the internal lifetimes be inferred.


#34

Don’t really want to open a syntax bikeshed but maybe, seems like it has pros and cons.


#35

What is that used for?

I assume it is only to make borrow-checker happy and doesn’t affect code generation in any way whatsoever, so I don’t understand why it’s a thing I should ever be aware of.


#36

Given a function signature like this:

fn foo(&self) -> T { ... }

I want to be able to deduce if the returned instance of T is allowed to outlive self or not. Unfortunately, due to lifetime elision, I already have to look at the type definition of T. If T is defined like this:

struct T { ... }

I can deduce that T is 'static and that the elided function signature is fn foo<'a>(&'a self) -> T.

If T is defined like this:

struct T<'a> { ... }

I know, from that signature, that T's lifetime is going to be tied to the lifetime of the self that I got it from, and that the elided function is fn foo<'a>(&'a self) -> T<'a>. I won’t be able to do as much with T as I would’ve been able to if it had been static.


#37

I see. But here the dissatisfaction is with function definitions, not struct definitions. Structs just pick up the burden. And struct lifetime declarations don’t even solve that problem well, because struct Foo<T> can still contain references (and T can be a trait hiding it multiple levels of indirection deep).

In such cases I just use the object and see if it compiles, because it’s much faster and gives a better answer than searching through the source code of the relevant library :slight_smile: Without an IDE looking up the struct is hard, but any IDE that can jump to definition, can also look up references in definitions.

I see there’s also a tension between what I want to write (no lifetimes at all if possible), and what I’d want to read (some lifetime annotations in places where it matters).

So maybe there are better solutions?

  • Rustdoc should add the explicit lifetime, so the code can be written as fn foo(&self) -> T {…}, but is documented as fn foo<'a>(&'a self) -> T<'a> (or maybe with a shorthand notation/icon, e.g. fn foo(&self) -> T<&>)

  • clippy should suggest using explicit lifetimes in function definitions if the relevant structs aren’t explicit about it

  • rustfmt should automatically add explicit lifetimes in places where they are non-obvious

In general, the compiler already knows this information. I’d rather have tools to bring it automatically, and only when and where it’s really needed, than be required to always provide it manually.


#38

One year later and I just come across this post. I have the following suggestion, not sure whether it is relevant or possible to implement or not: As far as I know, lifetime is used to match different references during assignment operator, so for struct, can we do something like bellow:

Case 1 : The struct only has one lifetime and all references inside the struct share that lifetime

        struct Foo<'_> {
             x : &i32, 
             y : &i32,
             z : &i32,
        }

And this should be equivalent to :

       struct Foo<'a> {
             x : &'a i32, 
             y : &'a i32,
             z : &'a i32,
        }

Some people have suggested something similar with this approach. The drawback is that all references inside the struct will be forced to have a same lifetime and that reduces the flexibility of all other data structs that shares some references with the above struct. This , however, covers quite a number of use cases, especially the simple cases.

Case 2 : Each reference in the struct has its own lifetime. In this case, the current explicit declaration of lifetime in struct can cause a viral effect, as we already know :

    struct Foo<'a, 'b, 'c> {
        x : &'a i32,
        y : &'b i32,
        z : &'c i32,
    }

    struct Bar<'a:'d, 'b:'d, 'c:'d, 'd> {
        foo : &'d Foo<'a, 'b, 'c>
    }

    fn get_x<'a,'b,'c,'d>(bar : &Bar<'a, 'b, 'c, 'd>) -> &'a i32 {
        bar.foo.x
    }

With the (unstable) #![feature(underscore_lifetimes)], we can make it a bit more simple:

    fn get_x<'a>(bar : &Bar<'a, '_, '_, '_>) -> &'a i32 {
        bar.foo.x
    }

This syntax is somehow similar to the following procedure style :

    fn get_x<'a, 'b, 'c>(x : &'a i32, y : &'b i32, z : &'c i32) -> &'a i32 {
        x
    }

We know that the compiler needs lifetime declaration to check the correctness of reference assignment. In the above case, only lifetime 'a is necessary to match the input and output lifetime of the function, and 'b & &c are redundant. Of coure, with procedure style, we can use lifetime elision:

    fn get_x<'a>(x : &'a i32, y : &i32, z : &i32) -> &'a i32 {
        x
    }

Why shouldn’t we try something similar to struct :

    struct Foo {
        x : & i32,
        y : & i32,
        z : & i32,
    }

    struct Bar {
        foo : &Foo
    }

    fn get_x(bar : &Bar) -> &'a i32 where bar.foo.x : &'a {
        bar.foo.x
   }

, as the compiler only needs to match the lifetime of the element bar.foo.x with the output result. Currently, the declaration bar : &Bar<'a, 'b, 'c, 'd> or bar : &Bar<'a, '_, '_, '_> makes it hard to deduce which element inside the struct does the lifetime annotation ('a in this case) corresponds to.