Simplification reference life-time

I would suggest Simplification reference life-time in Rust

Consider the following code:

fn foo<'a, 'b>(s1: &'a Vec<u8>, s2: &'b Vec<u8>) -> &'a Vec<u8> {
  println!("foo");
  s1
}

struct DoStruct {
    ll: u32
}

trait MyTrait<T> {
    fn do_something(self) -> &T;
}

impl<'a> MyTrait<u32> for &'a DoStruct {
    fn do_something(self) -> &'a u32 {
        println!("do_something");
        &self.ll
    }
}

As we can see, there are lots of declared reference life-time Why we should every time declare reference life-time ?! It is lots of boilerplate code !!

Consider simpler solution:

fn foo(s1: &'a Vec<u8>, s2: &'b Vec<u8>) -> &'a Vec<u8> { // fn foo<'a, 'b>(s1: &'a Vec<u8>, s2: &'b Vec<u8>) -> &'a Vec<u8>
  println!("foo");
  s1
}

struct DoStruct {
    ll: u32
}

trait MyTrait<T> {
    fn do_something(self) -> &T;
}

impl MyTrait<u32> for &'a DoStruct { // the same as impl<'a> MyTrait<u32> for &'a DoStruct
    fn do_something(self) -> &'a u32 {
        println!("do_something");
        &self.ll
    }
}

Compiler in this example deduce declaration of reference life-time by itself !! But it is possible to declare it manually :wink:

2 Likes

Your example code already compiles if you just remove all of the lifetimes: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=75cbeecd09629bf08c3e51bd5c856180

3 Likes

Okay, seems like I was not precise ... Consider the following example https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=645c862134060179ce10bf5af9b44c26

I believe you're basically suggesting https://github.com/rust-lang/rfcs/blob/master/text/2115-argument-lifetimes.md, which is accepted but not implemented.

Rust is really getting to a point where nearly every good feature request anyone has is already accepted-but-unimplemented. We really need to catch up on implementing this stuff.

19 Likes

You are completely right, it is what I suggest !! Cool that it is already accepted !!

But I am curious when it will be implemented ? What if I can help with implementation ?

There generally are no ETAs for unfinished features. It's almost never possible to predict that sort of thing unless the feature is already code complete and accepted for stabilization.

The tracking issue linked in the RFC is https://github.com/rust-lang/rust/issues/44524, which AFAICT doesn't say anyone's actively working on it atm. It's probably not a great first issue, but I imagine anyone who wanted to spend time trying to push it forward could ask for help there or in one of the Rust Zulip channels.

1 Like

@Ixrec

Does this RFC solve also this following issue ?

pub struct MyStruct {
}

pub trait A {
    fn a(&mut self);
}

pub trait B<'a>: A {
    fn b(&self);
    fn as_my_struct<'c>(&'c mut self) -> &'a mut MyStruct;
}

impl A for MyStruct {
    fn a(&mut self) {
        unimplemented!()
    }
}

impl<'a> B<'a> for MyStruct {
    fn b(&self) {
        unimplemented!()
    }

    fn as_my_struct<'c>(&'c mut self) -> &'a mut MyStruct {
        unimplemented!()
    }
}

But why I should explicitly write life-time 'c and -> &'a mut MyStruct in implementation if compiler could inference life-time for as_my_struct 'c and for -> &mut MyStruct and it will be possible to write the following code:

pub struct MyStruct {
}

pub trait A {
    fn a(&mut self);
}

pub trait B<'a>: A {
    fn b(&self);
    fn as_my_struct<'c>(&'c mut self) -> &'a mut MyStruct;
}

impl A for MyStruct {
    fn a(&mut self) {
        unimplemented!()
    }
}

impl B<'a> for MyStruct {
    fn b(&self) {
        unimplemented!()
    }

    fn as_my_struct(&mut self) -> &mut MyStruct {  // Should inference life-time fn as_my_struct<'c>(&'c mut self) -> &'a mut MyStruct by observing trait B
        unimplemented!()
    }
}

Not doing so we also violate DRY (Don't repeat yourself) https://en.wikipedia.org/wiki/Don't_repeat_yourself

If I have already declared life-time in trait, it should be optional to declare in implementation ...

@sfackler @Ixrec

Sometimes during refactoring such code:

struct CompositeObject {
    obj: SomeType,
}

struct BigObject {
    composite_obj: CompositeObject,
    count: i32,
}

struct Application {
   big_obj: BigObject,
}

developer decides to make obj of SomeType as reference in CompositeObject type:

struct CompositeObject<'a> {
    obj: &'a SomeType,
}

struct BigObject<'a> {
    composite_obj: CompositeObject<'a>,
    count: i32,
}

struct Application<'a> {
   big_obj: BigObject<'a>,
}

Everywhere in composition hierarchy I need to write 'a ... most of the times it is just boilerplate code ...

What if instead of writing manually we will introduce the 'self life-time:

struct CompositeObject {
    obj: &'self SomeType,
}

struct BigObject {
    composite_obj: CompositeObject,
    count: i32,
}

struct Application {
   big_obj: BigObject,
}

Code much simpler and more maintainable than fighting with named life-times in composite hierarchy :wink:

Compiler underhood will generate the following code:

struct CompositeObject<'self> { // 'self is implicit life-time of each struct, like this in other languages
    obj: &'self SomeType,
}

struct BigObject<'self> { // 'self is implicit life-time of BigObject
    composite_obj: CompositeObject<'self>, // Assign 'self of BigObject to CompositeObject
    count: i32,
}

struct Application<'self> { // 'self is implicit life-time of Application
   big_obj: BigObject<'self>, // Assign 'self of Application to BigObject
}

@sfackler @Ixrec
What do you think about such simplification ?

1 Like

The language is already moving in the other direction with '_.

With the 2018 edition, though the lint is allow-by-default, it's typically considered better practice to write

fn make_app(config: &Config) -> App<'_>

rather than

fn make_app(config: &Config) -> App

To make it clear that App<'_> captures a lifetime and is not an owned, 'static value.

Extending the "'_ as lifetime marker", I could see your example being written as

struct CompositeObject<'_> {
    obj: &SomeType,
}

struct BigObject<'_> {
    composite_obj: CompositeObject<'_>,
    count: i32,
}

struct Application<'_> {
   big_obj: BigObject<'_>,
}

The fact that these types capture a lifetime is important information that should be available locally rather than having to be known by global context.

I think the "real" solution is to just not use single-letter lifetimes. Instead, use meaningful names that help you understand the type. App<'a>: what does it borrow from, why isn't it an owned type? App<'config>: ah, it borrows the configuration from a different owner rather than owning it itself.

If the lifetime is trivial, it should be able to be elided (potentially with a little help from ('_). If it can't be elided, and you can't give it a meaningful name, you should be considering whether it's actually a meaningful borrow that expresses some relationship, or just trying to outmaneuver and fight the borrow checker.

9 Likes

@CAD97

But why ?

'self life-time would be very useful

'_ does not resolve the issue complete

I still need during refactoring to to write manually in each struct '_ - it is annoying because there is no special need for this

'_ with I need to write the same amount of boilerplate code as with 'a !!

Life-time '_ is good for not specifying the named life-time, but still require user of this struct to provide it

On the other hand 'self would be almost the same and user should not specify it manually

@CAD97

I have found that there was already the same proposal https://users.rust-lang.org/t/access-to-implicit-lifetime-of-containing-object-aka-self-lifetime/18917 to resolve the same issue with composite structs (deep level) :wink:

Seems like comunity releally need this feature !!

I personally need this feature, when I decide to use reference in struct I need to change whole hierarchy of composit struct and then after testing I figure out that reference bad choice and then start use Rc, but I will be enforced to remove all this reference life-times

It makes iterative programming a nightmare :frowning:

Also why user of my struct should care that there is somewhere in hierarchy reference if my library is just works and all checks was done by borrow-checker

Lifetimes are less about making code compile (which you seem to be focused on) and more about exposing an API to users that they can use. I care whether your type captures a lifetime because that controls what I can do with it, and I'd much rather know that from the signature than having to wait until I've designed and written some code to have the compiler tell me "actually no, you can't do that, because Application is not 'static" when you gave me no indication of this.

Quoting @ExpHP from the post you linked:

Perhaps a middle ground can keep clarity while still putting less notation burden on the user?

struct CompositeObject {
    obj: &'_ SomeType,
}

struct BigObject {
    composite_obj: CompositeObject<'_>,
    count: i32,
}

struct Application {
   big_obj: BigObject<'_>,
}

fn make_app(&SomeType, i32) -> Application<'_>;

The lifetimes are primarily there for you, the developer, not the compiler.

6 Likes

@CAD97

Okay, I got your point, but maybe we need to enfore only user to specify the '_ life-time ?

Consider the following example:

struct CompositeObject {
    obj: &'self SomeType,
}

struct BigObject {
    composite_obj: CompositeObject,
    count: i32,
}

struct Application {
   big_obj: BigObject,
}

fn make_app(&SomeType, i32) -> Application<'_>;

If user will try to use Application without specifying life-time it will get error something like this:

struct CompositeObject {
    obj: &'self SomeType,
}

struct BigObject {
    composite_obj: CompositeObject,
    count: i32,
}

struct Application {
   big_obj: BigObject,
}

fn make_app(&SomeType, i32) -> Application; # Error, please specify life-time, Application depends on implicitly added self life-time. One solution is to provide anonymous life-time, like that: Application<'_>

In such way it will be good enough from developer point of view as well as user point of view ...

What do you think ?

1 Like

@CAD97

Also if I would see such code:

pub fn make_foo(name: &str) -> Foo;
pub fn make_bar(name: &str) -> Bar;

or the following:

pub fn make_foo(name: &str) -> Foo;
pub fn make_bar(name: &str) -> Bar<'_>;

In both cases actually I do not know anything about behavior of make_bar and actually I should not care ... Instead, as seems for me, would be better to improve error message:

fn main() {
    let mut s = String::from("hello");
    
    {
        let foo = new_foo(&s);
        s.push_str(" world"); // ok
        println!("{:?}", foo);
    }
    
    {
        let bar = new_bar(&s);
        s.push_str(" world"); // ERROR: mutated while borrowed immutably at the line let bar = new_bar(&s), s is bound to Bar object
        println!("{:?}", bar);
    }
}

This message would be more useful than enforcing user to provide even anonymous life-time ...

Consider if I as developer just reuse other library type and if I write the following code:

pub fn make_foo(name: &str) -> Foo;
pub fn make_bar(name: &str) -> Bar; # Error, should be specified life-time

I would be shocked :wink:

Because I know nothing about Bar realization except of API ...

If you will enforce user or developer write <'_> each time it would be two steps froward, one step back )

I guess you do; it's apparently a shorthand exactly for

fn make_bar<'a>(a: &'a str) -> Bar<'a>

It seems worthy of note the following does not compile

fn make_bar(a: &str, b: &str) -> Bar<'_>
2 Likes

@atagunov

It was discussion about potential syntax ...

But anyway what the point of exposing to user to specify a life-time ?

What if life-time is related to private field in some deep deep structure hierarchy ?

Lets compiler will provide better error messages and we add 'self life-time ...

Lifetimes cannot be encapsulated. If your type has an input lifetime, then that is an important part of the type's API and a significant limiting factor on how it can be used.

I want to know if the types I'm using are owned ('static) or not by their signature, not by the compiler complaining at me some point down the line that I invalidated some reference I didn't even know was borrowed (because you want to hide the lifetime annotations).

When a user is first learning Rust and hasn't internalized borrowing rules yet, they probably rely on compiler errors to guide them through "fighting the borrow checker". Once you've internalized the rules, however, you can "run the borrow checker" in your head and stick to usage patterns that work within the borrowing rules.

By hiding lifetimes from signatures, you make the "fighting the borrow checker" problem worse. It's now impossible to know if some variable is borrowed until the compiler yells at you for misusing it.

Lifetime information in signatures is very useful information. Frankly, that you're trying to hide it (when, again, the presence of an input lifetime is un-encapsulatable complexity) suggests that you don't have a thorough understanding of how the borrow checker works.

Sure, we can make error messages more useful. But having that information locally obvious via annotation allows you to skip right past that phase where the compiler has to reject your code and slap your wrist.

8 Likes

For as long as you keep this deeply nested structure "alive" whatever you borrowed from is "locked".. Lifetimes are never conjured from thin air; they always "lock" something - in or above the current "scope".

Thanks I know it ...

My point is from user point of view I should not care of why Library author need my object to borrow ... I just need clear error message ...

You have proposed a plethora of changes and features in a quick succession recently, and it is evident from your posts you are missing several fundamentals of the language, the ecosystem, their goals, the idioms, and common solutions to common problems.

I'd suggest you to take a step back and instead of trying to change the language as the very first tool you reach for whenever you encounter a minor inconvenience, try to get more familiar with the language and the customs/style in detail so you understand how such small issues can be resolved with the already-rich toolset of Rust.

6 Likes