Parent lifetime (proposal for the 2024 edition of Rust)

Hello,

I don't know if this is already possible in Rust, but I had a little thought about lifetimes.

Why not add a specific lifetime 'parent , like 'static ?

I think it makes the developer's experience easier by making Rust less verbose.

I'll give you a few case studies :

struct TestA<'a> {
    val: usize,
    str: &'a str
}

fn main() {
    let t = TestA{
        val:10usize,
        str:"Hello"
    };
    println!("T>val = {}  ||  T>str = {}",t.val,t.str);
}

it would be easier that way :

struct TestA {
    val: usize,
    str: &'parent str
}

With 'parent we inform the compiler that we want str to have the same lifetime as its parent, i.e. TestA.

( 'parent lifetime takes maximum lifetime of Parent.)

What do you think?

3 Likes

In Rust, lifetimes (i. e. the system of those 'something marks in your code) only appear in types of references (or other types containing references, or trait objects), so for your struct TestA, it's unclear to me what you mean by “the same lifetime as its parent” or “maximum lifetime of Parent”, as TestA doesn't appear to have any lifetime to it anymore.

Of course I could try and guess as to what you might mean, but I can already come up with more than 3 entirely different things, and addressing each specutlatively seems inefficient.

As far as the example code you gave is concerned, that code works just as well if you had written 'static instead of 'parent, so the examples are, objectively, very lacking.

2 Likes

To note, while you can write e.g. fn f(_: TestA), style generally prefers writing fn f(_: TestA<'_>) instead, because the presence of a lifetime (the elided lifetime) is meaningful to what you can do with the value.

And oftentimes somewhat annoyingly, dyn Trait is dyn Trait + 'static and not dyn Trait + '_, so whether an elided lifetime is maximally short or maximally long can depend on the type which is eliding the lifetime.

The case with fn f(_: &TestA) is less clear, since the presence of a reference makes the presence of a lifetime evident, thus the lint being part of the rust_2018_idioms group yet still not on by default in the current edition.

1 Like

you're right, the example I gave isn't very relevant.

#[derive(Debug)]
struct TestA<'a> {
    val: usize,
    test_b: &'a TestB<'a>,
}

#[derive(Debug)]
struct TestB<'b> {
    str: &'b str,
}

pub fn main() {
    let testb = TestB { str: "Hello" };
    let testa = TestA {
        val: 10,
        test_b: &testb,
    };
    
    println!("TestA = {:?}",testa);
}

into this :

#[derive(Debug)]
struct TestA {
    val: usize,
    test_b: &'parent TestB,
}

#[derive(Debug)]
struct TestB {
    str: &'parent str,
}

pub fn main() {
    let testb = TestB { str: "Hello" };
    let testa = TestA {
        val: 10,
        test_b: &testb,
    };
    
    println!("TestA = {:?}",testa);
}

the aim is that when the compiler detects the 'parent keyword in a field, it considers the struct to have a default lifetime ('parent).

struct TestA {
    val: usize,
    test_b: &'parent TestB,
}

the compiler will automatically deduce the lifetimes and transform this code into :

struct TestA<'parent> {
    val: usize,
    test_b: &'parent TestB<'parent>,
}

you can also push automation to the max, by removing the lifetime,

if there is no lifetime, the compiler considers it to be ('parent) by default : &xxx to &'parent xxxx

ex:

struct TestA {
    val: usize,
    test_b: &TestB,
}

// converted to :
struct TestA {
    val: usize,
    test_b: &'parent TestB,
}

// converted to again :

struct TestA<'parent> {
    val: usize,
    test_b: &'parent TestB<'parent>,
}
3 Likes

Thanks for sharing this better explanation, it makes much more clear what we are talking about.

I would personally side with the style preferences @CAD97 already quoted above, that lifetimes - even elided ones - should best be kept visible (except for types like references which are syntactically clear to spot and always have a lifetime, anyway).

This means that I would say I prefer the style that any struct with some lifetime-having fields is also declared like

struct Name<…something lifetime-looking here…> { … }

rather than just struct Name { … }.

I can still understand the desire to somehow make the thing look less explicit. After all, struct/enum definitions are one of the relatively few kinds of places for lifetimes where no elision rules exist at all.

On the other hand, the explicitness has advantages. For one thing, compared to the existing cases of “elision” sugar for lifetimes, (though your proposed feature doesn’t use the same syntax it still feels like a related idea), the existing elision usually introduces distinct lifetime parameters everywhere[1], whereas this 'parent lifetime introduces the same lifetime parameter everywhere. So this is a bit of an “inconsistency”. Furthermore, having the same lifetime in multiple places is usually a restriction/constraint and can – if they weren’t what was actually needed – lead to compilation errors later down the line (perhaps you needed a struct with two (or more?) distinct lifetime parameters after all…). Especially in the &'a mut SomeType<'a> anti-pattern case, which looks like your example code above suggests could then be written just “&'parent mut SomeType”. So I prefer the more explicit existing syntax, where those relations between the multiple appearances of the same lifetime are just a bit easier to spot.


  1. except for function return types, which naturally must - for most common/reasonable code - match some of the argument lifetimes ↩︎

8 Likes

How about allowing the (already special) lifetime '_ in the generic parameter list, to indicate the lifetime parameter that should be used for elided lifetimes within the definition?

That would make the examples look something like this:

struct TestA<'_> {
    val: usize,
    test_b: &TestB<'_>,  // or just `&TestB`, but this would trigger the existing elided-lifetime lint
}

struct TestB<'_> {
    str: &str,
}

Does '_ offer any advantage over 'a in this case?

1 Like

From a usage perspective, it behaves exactly like 'a does now. The only (minor) advantage is within the definition:

'_ alerts the reader that there may be elided lifetimes within the body, which has not been previously allowed, and if there are multiple lifetime parameters, marks which one is being used to fill the elided places in the definition.

The fact that all the lifetimes get assigned to the only parameter seems pretty footgunny to me since this often leads to errors.

6 Likes

I understand your point of view, and it's totally logical. For pro Rust developers, it's perfectly normal, but for beginners or newcomers (especially from languages with GC [Java, C#, Go, etc.] it will make the transition easier).

[[Sometimes it's easy to end up with declarations consisting of generics plus lifetimes, which makes the code difficult to read.]]

Personally, I find Rust simpler than Go, since I've acquired the right reflexes (Mutex,Rwlock,etc..); but it took me about 5 months to get used to it. In the business world, TTM (time to market) is crucial when choosing a technology.

The two notions in Rust that confuse new language entrants are Borrowing and Lifetimes. Finding the right formula to mask this complexity is like finding the long-sought model for merging classical and quantum physics. :laughing:

In any case, it was a thought I had that I wanted to share with you.

Thank you for your feedback,

1 Like

As someone who helps out newcomers with lifetime problems quite a lot, I don't agree that hiding the lifetimes is the answer to make learning borrowing and lifetimes easier. If the problem isn't trivial, the first thing I do is #![deny(elided_lifetimes_in_paths]. And a lot of the time, a newcomer mistake is overusing lifetimes (references really). Making it easier to ignore that they exist upfront and making it harder to see where they are isn't going to improve that.

The language actually just took a step backwards in this regard with fully capturing RPITIT. I think half or more of the issues I've helped people with using that feature are related to overcapture (invisble lifetime capture by the return type).

8 Likes

Very interesting what you're saying, so it's simpler to consider Rust as a break with all the paradigms taught and see it as a new beginning.

I'd have to really sit down and think to be able to articulate it well. But I do know pretending that references / borrowing structures can be treated the same as owning data structures doesn't go well (because it's not true).

7 Likes

It's not so much that it's a break with all the paradigms taught (when I learnt assembly, for example, I was trained to treat owned data very differently to data that I merely had a pointer to, and that carried over into C and C++ later), it's that Rust has language-level enforcement of the idea that owning data is different to borrowing it.

This isn't something you're taught about if you're only ever taught about GC languages (it wasn't something that came up when I was taught Java at university), but it is something you learn if you're explicitly taught systems programming languages (in my case, Z80 and 6502, followed by ARM2, C, 8051 and C++).

7 Likes