Lets remove explicit lifetimes

Hello. Rust is very cool language and it is easy and flexible, because it is more abstract by ideas from functional programming, but rust has almost just one but - explicit lifetimes. Very often my ideas brake on this lifetimes.

That's why they have confusing syntax and they make code dirtier.

Okey, I can explain and understand case

struct Foo<'a>{
    field:&'a i32,
}

impl<'a> Foo<'a>{}

But what mean when we have?

struct Foo<'a,'b>{
    field1:&'a i32,
    field2:&'a i64,
}

or
fn foo<'a,'b>(s:&'a Foo<'b>) -> Foo<'a>

or
struct Foo<'a,'b,T:Trait + 'b>

or what is should do if i want 2 fields with same generic but different lifetimes?(I do not understand more then one lifetimes>

struct Foo<'a,'b,T1:Trait<'a>+'a,T2:Trait<'a>+'b>{
    field1:T1,
    Field2:T2,
}

and this instead
struct Foo<T>

and how about this:

struct Foo<'a,'b,T:Trait<'a>+'b>{
    prev:Option<&'a Foo<'a,'b> >,
    value:&'b T,
}

Often I am scared to recommend rust - lifetimes are too problematic.

I am sure, that where are only a little rust developers, who understand lifetimes so good und who do nod spend 1-3 hours for 20 lines of code with lifetimes. For others this is way to shot in leg.

I am sure that almost nobody needs them. As for me, this is just new routine troubles in coding. I also heard discontent about it.

This is really good way to spend more than one day. This is only thing I need to ask about help in rust! This is very long minus for industrial development. A lot of programmers have not heard about closures and tuples, but this lifetimes make rust the most difficult language I know. Just because you can write something with c++ and it will works, but rust will make suffer with lifetimes. Explicit lifetimes is bad idea. Please, remove them as soon as possible or make it optional or change lifetimes syntax(for example like type A:Trait in traits, and > < for lifetime comparison.

I agree with you. Adding a simple reference to a struct causes an explosion of lifetime annotations, even in a basic case.

In Rust there are already some ways to avoid this, e.g. instead of:

struct<'a, T:'a> {field: &'a T}

with T=SomeType use

struct<T> {field:T}

with T=&SomeType. Same for traits. Don't make references explicit unless absolutely necessary.

2 Likes

Actually, no. Did you know that elision works on structs, too?

struct Foo<'a> {foo: &'a u8}

fn make_foo(x: &u8) -> Foo /* no lifetime! */ {
    Foo {foo: x}
}

Most folks aren't aware of this and start adding annotations everywhere, but it's not that necessary.

9 Likes

Explicit lifetime syntax is kind of necessary for Rust to work. Without it you can't differentiate between cases where multiple values are expected to have the same lifetime vs those which are allowed to have different lifetimes. It's a relatively common kind of bug in C++ where you take a pointer and put it inside a container which lives longer than the original pointer -- explicit lifetimes help prevent this.

Now, we could elide explicit lifetime on simple structs -- this has been discussed many times in the past, with no clear result.

It seems like you haven't quite grasped how lifetimes work. Could you go through http://rust-lang.github.io/book/ch10-03-lifetime-syntax.html (the new book's chapter on lifetimes)?

I don't quite understand what you're asking in the rest of the post. The examples seem contrived -- while I've seen a lot of complicated lifetime code in my time, never something with lifetimes on traits being used like that.

9 Likes

I think, lifetimes may be derived automatically. For example, & in struct may live more, then struct. And show me example , what mean multiple lifetimes for one struct. I do not see it in lifetime-syntax, you cited.
For example, I am waiting answers for my post Explicit lifetimes troubles already 3 hours. Why? Because lifetimes are easy and anybody understands it. If you understand, this should be easy for you. Just do it =)

I've found it useful to think of generic lifetime parameters as constraints - they're not representing concrete lifetimes (rust fills those in on its own) but rather a relationship. It's not much different (in principle) than generic type parameters with bounds (constraints) - they're just different and new because no other mainstream language has that facility.

2 Likes

I "think" that lifetimes can be easy if you don't overthink them.
Let's say you must anotate your struct like so:

struct Foo<'a> {
   field: &'a i32,
}

It basically says, when you instantiate your struct it starts its "lifetime" and lives at least 'a duration:

[Foo::new()]                                 ['a]
|--------------------------------------------|
|-------------------------------------------------> time

If I'm not mistaken, than

fn foo<'a, 'b>(s:&'a Foo<'b>) -> Foo<'a> { /*...*/ }

could mean either of these:
1.) foo returns a Foo which ones scope terminates sooner

[let f = Foo::new();]                                 ['b]
|----------------------------------------------------| 
               [let f2 = foo(f);]        ['a]
               |-------------------------|
|-----------------------------------------------------------> time

2.) foo returns a Foo where the argument's scope terminates sooner

[let f;]                                             ['a]
|----------------------------------------------------|
         [let f2 = Foo::new(); ]              ['b]
         |------------------------------------|
               [f = foo(f2);]                  
               || <- just a function call during 'b scope
|-----------------------------------------------------------> time

3.) 'a and 'b are the same, meaning they live in the same scope

Or other way to look at them is like digital signals, where operation can be done only if the time functions overlap, and { marks a "rising edge" and } a "falling edge" like:

___|───────────────────────|______
______|──────────────────|________
      |<-- call foo() -->|

It's just drawing out scopes along a time axis, and thinking about a few constrains which comes with &, &mut.

I'm still a newbie, but thinking about lifetime params like the above diagrams makes it easy to at least figure out why do I have to be explicit. Maybe it helps you too. Anyhow, correct me if I'm terribly wrong. :slight_smile:

3 Likes

Here's how I think of it :slight_smile:.

When you have

struct Foo<'a> { field: &'a i32 }

The 'a is like a generic type parameter. In other words, consider the following:

struct Foo<T> {
    field: T
}

You need to declare the T type parameter in order to use it in the definition. Likewise, you need to declare the generic lifetime parameter 'a to make use of it in the definition. So, the &'a i32 is saying that the reference outlives any instance of a given Foo; that makes sense, of course, because a Foo cannot be left to hold a reference to something that's been dropped/freed/etc. So in other words, any instance of Foo lives at most 'a - it cannot outlive the reference it's holding. We don't care what the 'a is precisely (it's like a generic type parameter T - we don't necessarily care what it is), but we constrain Foo's lifetime by it.

Now, any particular instance of Foo can have a different lifetime. It can be anonymous (i.e. Rust assigns it), or you can declare a 'static if that makes sense for the situation.

fn foo<'a,'b>(s:&'a Foo<'b>) -> Foo<'a> {...}

As I mentioned in an earlier post, you can look at the generic lifetime parameters as constraints. Here, we have a reference to a Foo<'b> with lifetime 'a. One thing we can infer from this is that 'b : 'a. This also makes sense: to get a reference to a Foo, you must have a Foo in scope (i.e. it has to exist in order to grab a reference to it). When that Foo instance was created, it was given a &i32, which means the i32 must've existed.

2 Likes

But that's not what lifetimes are for! You're not saying that the & in the struct lives longer with the explicit lifetime. The 'a in Foo<'a> is not the lifetime of the struct. When you say struct Foo<'a> {x: &'a u8} you're saying that "Foo contains data tied to a scope -- some scope -- of lifetime 'a" and then you say that internally this data is an &u8.

Multiple lifetimes let you differentiate between Foo<'a> {x: &'a u8, y: &'a u8} where it contains two references of the same lifetime, and Foo<'a, 'b> {x: &'a u8, y: &'b u8} where it contains references of different lifetimes. This doesn't have to do with the "lifetime of Foo" at all. As @vitalyd mentioned, this is exactly like how type parameters work. Foo<T> is "Type Foo contains a type T -- some type T", and Foo<'a> is "Type Foo contains data tied to a scope 'a -- some scope 'a". T is not the "type of foo", and 'a is not the lifetime of Foo.

4 Likes

It's not really of "the same lifetime" though, right? It just means that both refs are live while any given Foo instance is live. So, for example:

struct Foo<'a> {
    field: &'a i32,
    field2: &'a i32
}

fn main() {
    let x = 5;
    {
        let y = 10;
        let f = Foo {field: &x, field2: &y};
    }
}

x and y have different (concrete) lifetimes, but the code above is sound. That's why I like to think of lifetime parameters like the above as a constraint (or relationship).

The lifetimes get squeezed to be the same lifetime once inside Foo, however. The sameness/difference between lifetimes gets more important when it comes to mutability.

The fact that Foo is not allowed to live longer than its contained lifetimes is an additional, different constraint.

Could you expand on that please?

If one of those refs was mutable, then whether or not you can set data borrowed from the other ref to something it contains depends on if the lifetimes are same or different.

(The stretchy-squeeziness of lifetimes also changes when mutation is in the picture, see Subtyping and Variance - The Rustonomicon)

2 Likes

But doesn't this just require that the mutable ref doesn't outlive the immutable ref? The concrete lifetimes can still differ so long as that relationship/constraint holds up, no?

Yes, you need to explicitly specify a 'b: 'a relationship then.

I aslo find explicit lifetime is the hardest part of Rust, I wish the doc can cover the common design pattern for not only lifetime but also when use value, when use reference when use Box, RefCell to finish common work.

1 Like

I know the feeling, however I think the topic is too broad to show common design patterns. What to use depends too much on the situation. I feel like the best the doc can do is to show you clearly what the individual moving pieces can do, and let you adapt them to your use case.

That said, plenty examples don't hurt!

3 Likes

I'd like to add one thing to this discussion: Rust often makes things a bit more complex so one can reason locally about the code. This includes restricting type inference to functions and keeping lifetime elision simple. Of course, one could try to infer lifetimes (using whole-program reasoning), but the results may be counter-intuitive at times, and even harder to reason about than with explicit lifetimes.

I should note that there's a proposal to allow lifetimes being even more explicit, by adding the option of ascribing lifetimes to references.

6 Likes

Have you taken a look at the New Rustacean podcast by Chris Krycho? He has some pretty good materials pertaining to those problems and a lot of others for people new to the language or who are just hung up on a particular issue.

1 Like

Could clippy help with this? For example, a redundant-lifetime ...

I decided to check and of course this already exists! needless-lifetimes :smile:

4 Likes