Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hierarchic anonymous life-time #2949

Closed
wants to merge 12 commits into from

Conversation

redradist
Copy link

No description provided.

@Ixrec
Copy link
Contributor

Ixrec commented Jun 24, 2020

This proposal was previously discussed at https://internals.rust-lang.org/t/simplification-reference-life-time/12224/8?u=ixrec, but unfortunately it looks like none of the problems raised there have been addressed. To avoid relitigating what those problems are, here's a quick summary:

  • The motivation cited is iterative development, but it's well-known that when prototyping/iterating, references in structs are best avoided in favor of cloning or boxing until the final design becomes clear.
  • Lifetimes are semantically viral, cannot be encapsulated, or however one wishes to phrase that. Omitting the viral syntax without any change to the semantics just creates structs that misleadingly appear to have no lifetime parameter even though they actually still have one. Historically this omission was allowed on return types, but that proved so confusing in practice we've already deliberately reintroduced a '_ syntax for that (as even the RFC acknowledges).
  • Some posts in the linked thread imply part of the motivation is a poor error message, but this change would only make it harder to produce a good error message (for the reasons in the previous bullet point)
  • This doesn't even work for structs with multiple lifetime parameters. (EDIT: you're right, I misunderstood this part)
  • While there are past suggestions/proposals for a 'self lifetime, they all seem to be aiming at different problems (e.g. enabling self-referential structs), and on closer inspection all of them turned out to be incomplete or confused.
  • Having to write <'a> two times on each struct in a composition hierarchy just isn't a big enough problem in practice to justify dedicated sugar anyway.

@Ixrec
Copy link
Contributor

Ixrec commented Jun 24, 2020

FWIW, I could get behind a proposal to allow struct Foo<'_> { b: Bar } where Bar has an omitted lifetime parameter, since struct Bar<'_>{} would still needs a '_. That does cut the typing in your example from <'a><'a> to just <'_>, seems like a place we'd expect '_ to work anyway (or at least I did; I had to go check that it doesn't already work), and doesn't hide the fact that Foo has a lifetime parameter. But that's as far as I could see us ever going with this sort of idea.

@redradist
Copy link
Author

@Ixrec

This proposal was previously discussed at https://internals.rust-lang.org/t/simplification-reference-life-time/12224/8?u=ixrec, but unfortunately it looks like none of the problems raised there have been addressed. To avoid relitigating what those problems are, here's a quick summary:

* The motivation cited is iterative development, but it's well-known that when prototyping/iterating, references in structs are best avoided in favor of cloning or boxing until the final design becomes clear.

* Lifetimes are semantically viral, cannot be encapsulated, or however one wishes to phrase that. Omitting the viral _syntax_ without any change to the semantics just creates structs that misleadingly appear to have no lifetime parameter even though they actually still have one. Historically this omission was allowed on return types, but that proved so confusing in practice we've already deliberately reintroduced a `'_` syntax for that (as even the RFC acknowledges).

* Some posts in the linked thread imply part of the motivation is a poor error message, but this change would only make it harder to produce a good error message (for the reasons in the previous bullet point)

* This doesn't even work for structs with multiple lifetime parameters.

It will work with multiple life-times as well as without life-time:

struct CompositeObject<'a> {
    counter: &'a usize,
    obj: &'self SomeType,
}
struct BigObject<'a> {
    composite_obj: CompositeObject<'a>,
    count: i32,
}
struct Application<'a> {
   big_obj: BigObject<'a>,
}

This code will be translated to:

struct CompositeObject<'a, 'self> {
    counter: &'a usize,
    obj: &'self SomeType,
}
struct BigObject<'a, 'self> {
    composite_obj: CompositeObject<'a, 'self>,
    count: i32,
}
struct Application<'a, 'self> {
   big_obj: BigObject<'a, 'self>,
}

'self is implicitly added and it will allow even in iterative programming to use references ;)

* While there are past suggestions/proposals for a `'self` lifetime, they all seem to be aiming at different problems (e.g. enabling self-referential structs), and on closer inspection all of them turned out to be incomplete or confused.

Why ? Inspection will show the line where object implicitly bound with some other object

* Having to write `<'a>` two times on each struct in a composition hierarchy just isn't a big enough problem in practice to justify dedicated sugar anyway.

It is not two times ))) It is two times in example of code ... I have been writing library where it was a real issue ... Each time I wanted to test some new design and I had to change half of the code to test it and then better design was to change it little-bit and it was a nightmare

@redradist
Copy link
Author

@Ixrec

FWIW, I could get behind a proposal to allow struct Foo<'_> { b: Bar } where Bar has an omitted lifetime parameter, since struct Bar<'_>{} would still needs a '_. That does cut the typing in your example from <'a><'a> to just <'_>, seems like a place we'd expect '_ to work anyway (or at least I did; I had to go check that it doesn't already work), and doesn't hide the fact that Foo has a lifetime parameter. But that's as far as I could see us ever going with this sort of idea.

It will work simlicly with '_ anonymous life-time like this:

struct Foo<'_> {
    b: Bar
}

struct Bar<'_>{
   obj: &'self BigObj,
}

and it will translated to:

struct Foo<'_, 'self> {
    b: Bar<'_, , 'self>
}

struct Bar<'_, 'self>{
   obj: &'self BigObj,
}

@zackw
Copy link
Contributor

zackw commented Jun 25, 2020

I want to highlight some bits of back-and-forth above that I think point right at the crux of why @redradist and everyone else are talking past each other:

@Ixrec wrote:

Lifetimes are semantically viral, cannot be encapsulated, or however one wishes to phrase that. Omitting the viral syntax without any change to the semantics just creates structs that misleadingly appear to have no lifetime parameter even though they actually still have one.

@redradist wrote:

Each time I wanted to test some new design and I had to change half of the code to test it and then better design was to change it little-bit and it was a nightmare

It seems to me, @redradist, that you have not understood why Rust has lifetime parameters in the first place, and so you're trying to avoid thinking about the exact thing that lifetime parameters exist to require you to think about.

Your hypothetical pair of Application structs...

struct Application<'a> { ... }
struct Application { ... }

have to be used in profoundly different ways. I deleted the contents of the structs because they don't matter. The presence or absence of the lifetime parameter is relevant to the client of the API and that's why we want it to be explicit. Specifically, this function

fn create_application() -> Application {
   Parameter param { ... };
   Application { ..., param, ... }
}

is correct (and will compile, assuming Parameter is a Copy type) for struct Application with no lifetime parameter, but incorrect for struct Application<'a> (and won't compile, even with &param) because you're returning an object containing a reference to another object that no longer exists after the function returns.

You're frustrated because you keep messing with the internals of Application in ways that change whether it needs a lifetime parameter and having to change every nested structure, and that frustration is Rust trying to show you that you're doing your API design backward. You shouldn't allow the internals of Application to dictate whether it has lifetime parameters. You should consciously decide whether or not Application's external API ought to include lifetime parameters, based on what makes most sense for how Application should be used, and then propagate that decision downward into the internals.

@burdges
Copy link

burdges commented Jun 25, 2020

I think struct Foo<'_> { b: Bar } harms readability too much @Ixrec

@redradist You'll reduce refactorings by initially writing

struct Foo<B> { b: B, .. }
impl<B: Borrow<Bar>> Foo<B> { .. }
impl<B: BorrowMut<Bar>> Foo<B> { .. }

@redradist
Copy link
Author

redradist commented Jun 25, 2020

I want to highlight some bits of back-and-forth above that I think point right at the crux of why @redradist and everyone else are talking past each other:

@Ixrec wrote:

Lifetimes are semantically viral, cannot be encapsulated, or however one wishes to phrase that. Omitting the viral syntax without any change to the semantics just creates structs that misleadingly appear to have no lifetime parameter even though they actually still have one.

@redradist wrote:

Each time I wanted to test some new design and I had to change half of the code to test it and then better design was to change it little-bit and it was a nightmare

It seems to me, @redradist, that you have not understood why Rust has lifetime parameters in the first place, and so you're trying to avoid thinking about the exact thing that lifetime parameters exist to require you to think about.

Do not make fast conclusion ;) My background started from C/C++ where the not tracking life-time was an issue

On the other hand Rust has very cool and nice idea about life-times and by tracking them it can know when using some variable can 'cause and issue ... But as drawback Rust was designed in such way that it enforce developers to explicitly add named life-times that is not bad idea in some cases for example in dependent life-times:

#![feature(label_break_value)]

struct MyStruct<'a, 'b: 'a> {
    k: &'a i32,
    i: &'b u32,
}

fn main() {
    'a: {
        let i: u32 = 3;
        'b: {
            let k: i32 = 3;
            let struc = MyStruct { k: &k, i: &i };
        }
    }
}

But most of the time writing life-time is just boilerplate ... all structs bypass life-times like this:

struct MyStruct<'a> {
    k: &'a i32,
    i: MyStruct2<'a>,
}

struct MyStruct2<'a> {
    i: &'a u32,
}

fn main() {
    'a: {
        let i: u32 = 3;
        'b: {
            let k: i32 = 3;
            let struc = MyStruct { k: &k, i: MyStruct2 { i: &i } };
        }
    }
}

I do not think that named life-times it is bad idea in fact I like them, but in certain cases they unnecessary and just reduce code readability and maintainability (

Your hypothetical pair of Application structs...

struct Application<'a> { ... }
struct Application { ... }

have to be used in profoundly different ways. I deleted the contents of the structs because they don't matter. The presence or absence of the lifetime parameter is relevant to the client of the API and that's why we want it to be explicit. Specifically, this function

fn create_application() -> Application {
   Parameter param { ... };
   Application { ..., param, ... }
}

is correct (and will compile, assuming Parameter is a Copy type) for struct Application with no lifetime parameter, but incorrect for struct Application<'a> (and won't compile, even with &param) because you're returning an object containing a reference to another object that no longer exists after the function returns.

You're frustrated because you keep messing with the internals of Application in ways that change whether it needs a lifetime parameter and having to change every nested structure, and that frustration is Rust trying to show you that you're doing your API design backward. You shouldn't allow the internals of Application to dictate whether it has lifetime parameters. You should consciously decide whether or not Application's external API ought to include lifetime parameters, based on what makes most sense for how Application should be used, and then propagate that decision downward into the internals.

Okay, I got your point, but what is bad in mixing two solution: explicit life-time (for user) and implicit self (for developer)

fn make_app(config: &Config) -> Application<'_>; // 'self was added as hidden life-time due to internal struct has reference

It has all advantages as from user as from developer point of view ...

@redradist
Copy link
Author

redradist commented Jun 26, 2020

@Ixrec @zackw @burdges

Reading all the comments on pull-request and in discussion, I have figured out that I was wrong in name 'self

I mostly mean some life-time that is implicitly added to structure declaration

Seems like I confused all of you with name 'self, sorry ;)

The syntax also could be something like this:

struct CompositeObject<'a> {
    counter: &'a usize,
    obj: &'_ SomeType,
    obj2: &'_ SomeType,
}
struct BigObject<'a> {
    composite_obj: CompositeObject<'a>,
    count: i32,
}
struct Application<'a> {
   big_obj: BigObject<'a>,
}

and translation to:

struct CompositeObject<'a, 'anon0, 'anon1> {
    counter: &'a usize,
    obj0: &'anon0 SomeType,
    obj1: &'anon1 SomeType,
}
struct BigObject<'a, 'anon0, 'anon1> {
    composite_obj: CompositeObject<'a, 'anon0, 'anon1>,
    count: i32,
}
struct Application<'a, 'anon0, 'anon1> {
   big_obj: BigObject<'a, 'anon0, 'anon1>,
}

Maybe this syntax much better, because it does not confuse anyone ....

'self as concept could be implemented differently like this:

struct CompositeObject<'a> {
    counter: &'a usize,
    obj: &'self SomeType,
}
struct BigObject<'a> {
    composite_obj: CompositeObject<'a>,
    count: i32,
}
struct Application<'a> {
   big_obj: BigObject<'a>,
}

'self life-time is smaller or equal than life-time of CompositeObject object

If 'comp_obj is life-time of CompositeObject than 'comp_obj: 'self

@redradist redradist changed the title 'self life-time Hierarchic anonymous life-time Jun 27, 2020
@redradist
Copy link
Author

@Ixrec @zackw @burdges I have changed the name and the wording in RFC

# Drawbacks
[drawbacks]: #drawbacks

Not known at the current time

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several drawbacks mentioned in the corresponding thread on IRLO and the comments on this PR. It would be helpful to list them here to get a better overview.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pythoneer I will add ;)

Comment on lines +83 to +86
struct CompositeObject {
obj0: &'_ SomeType,
obj1: &'_ SomeType,
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
struct CompositeObject {
obj0: &'_ SomeType,
obj1: &'_ SomeType,
}
struct CompositeObject<'_> {
obj0: &SomeType,
obj1: &SomeType,
}

Hidden lifetimes are deprecated, you also don't ever want '_ on references, as that's implied.

Copy link
Author

@redradist redradist Jun 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CryZe
Why they deprecated ?

What if the obj0 and obj1 would have different life-times ? How it will work with single anonymous life-time in declaration ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@redradist Why not? Both of them could depend on the same lifetime and life as long as each other.

Copy link
Author

@redradist redradist Jun 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@redradist Why not? Both of them could depend on the same lifetime and life as long as each other.

@pickfire But what would be semantic of struct CompositeObject<'_> ? Does '_ mean one life-time or it mean several different life-times ? What if obj0 and obj1 has different life-times ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true even though it is uncommon.

big_obj: BigObject<'a>,
}
```
Everywhere in composition hierarchy I need to write 'a ... most of the times it is just boilerplate code ...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Everywhere in composition hierarchy I need to write 'a ... most of the times it is just boilerplate code ...
The developer need to write `'a` throughout the hierarchy.

Comment on lines 67 to 78
struct CompositeObject<'anon> { // 'anon is implicitly added life-time
obj: &'anon SomeType,
}

struct BigObject<'anon> { // 'anon is implicitly added life-time
composite_obj: CompositeObject<'anon>, // 'anon is implicitly used here
count: i32,
}

struct Application<'anon> { // 'anon is implicitly added life-time
big_obj: BigObject<'anon>, // 'anon is implicitly used here
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the following code is generated, how will a reader knows that

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

have an implicit CompositeObject<'anon> by reading the documentation generated for BigObject?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the following code is generated, how will a reader knows that

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

have an implicit CompositeObject<'anon> by reading the documentation generated for BigObject?

@pickfire Actually thinking ... what explicit life-time gives you ? You just need to know how to initialize CompositeObject ... all other job should be done by CompositeObject, it is level of abstraction

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me it has been quite helpful to see explicit lifetimes because then I know that there is a lifetime being tracked inside the structure. In that way, it is a very helpful kind of documentation. One that is also always up-to-date, which is not a common feature of documentation in general.

Copy link
Author

@redradist redradist Jul 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me it has been quite helpful to see explicit lifetimes because then I know that there is a lifetime being tracked inside the structure. In that way, it is a very helpful kind of documentation. One that is also always up-to-date, which is not a common feature of documentation in general.

@felix91gr You answer a question: "... it is a very helpful kind of documentation ...", it is mix of multiple stuffs in one ... it breakage of lot of pattern like SOLID - Single Responsibility

Lets not mix life-time with good documentation ... from my point you should only know how to initialize CompositeObject that is all

Responsibility of proper handling life-time is responsibility of CompositeObject and developer that written it

Bypassing life-time from "low-level" structures to "high-level" structures (logically) is violation of layers responsibilities

Each abstract layer souls be responsible for its stuff

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it breakage of lot of pattern like SOLID - Single Responsibility

Those patterns were created as heuristics for Object-Oriented Programming, not as tips for general design.

In this case, having two responsibilities is good - it puts the information in exactly one place, instead of two. Keeping the documentation of what this lifetime is affecting becomes trivial once you have to keep it written, doesn't it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the lifetime needs to be in the docs. It is necessary for the user to know about the lifetime as it massively affects the usage of the struct. You need to design your code in a way that is compatible with the struct's lifetime semantics. Hiding those is incredibly dangerous in that it might mislead the developer into designing code that might not compile at all. Hiding lifetimes has already been deprecated, you are supposed to use at least '_ nowadays, as it clarifies that a lifetime relationship is going on. So the absolute minimum that might make this RFC acceptable would be struct Foo<'_>. <'_ ...> probably is not working either, as that either just means the same thing as <'_> or it is incredibly fragile.

Take a look at examples above with syntax like this:

struct CompositeObject& {
    obj0: &SomeType, # Deduce life-time for this reference
    obj1: &SomeType, # Deduce life-time for this reference
}

or even just notification that in future structure will use references:

struct CompositeObject& { // No references inside, but may will be in future
    obj0: SomeType,
    obj1: SomeType,
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe at this point the RFC is basically lifetime elision in type declarations with & instead of '_ (which imo there's no reason to introduce new syntax when we already have '_ for this. And additionally it introduces automatic elision for 'static which is probably a bit more controversial (and should probably be a separate RFC afterwards).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe at this point the RFC is basically lifetime elision in type declarations with & instead of '_ (which imo there's no reason to introduce new syntax when we already have '_ for this. And additionally it introduces automatic elision for 'static which is probably a bit more controversial (and should probably be a separate RFC afterwards).

There is on big difference '_ works and show only that in structure used only one reference with one life-time, but CompositeObject& shows that in structure used some references without explicitly declared life-time and number of fields could be different: 2, 3 ... whatever

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't, you can use a single lifetime but many references. Having more than one lifetime in a type's definition is only useful for disambiguating them for the user, but you can't do that with & anyway.

Copy link
Author

@redradist redradist Jul 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CryZe

It doesn't, you can use a single lifetime but many references. Having more than one lifetime in a type's definition is only useful for disambiguating them for the user, but you can't do that with & anyway.

Okay, will it be possible to use '_ life-time without reference inside ?

struct CompositeObject<'_> { // No references inside, but may will be in future
    obj0: SomeType,
    obj1: SomeType,
}

Co-authored-by: Ivan Tham <pickfire@riseup.net>
Comment on lines 47 to 61
What if instead of writing manually we will specify reference fields with anonymous life-time:
```rust
struct CompositeObject {
obj: &'_ SomeType,
}

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

struct Application {
big_obj: BigObject,
}
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that you need some notation on each point of use to specify that a lifetime was elided. That is, CompositeObject isn't a type with no lifetime parameters, it's a type whose exact lifetime parameters were deemed unimportant. So in struct BigObject, when defining a field with this type, we could write CompositeObject<'_> to mark this.

But even this wouldn't be sufficient: this feature wants to abstract away the number of lifetime parameters too (we could have two fields with elided lifetimes). So maybe CompositeObject<'_ ...> works? Where '_ ... means any number of lifetime parameters (that gets implicitly added to BigObject's own parameters). Likewise for Application.

Now, you perhaps need some notation in the struct definitions too to specify that there are N elided lifetimes. For symmetry, struct BigObject<'_ ...> works.

With those additions, the feature seems less appealing. But without them, it's confusing.

}
```

Code much simpler and more maintainable than fighting with named life-times in composite hierarchy
Copy link
Contributor

@pickfire pickfire Jul 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Code much simpler and more maintainable than fighting with named life-times in composite hierarchy
Code much simpler and easier to refactor throughout hierarchy of named lifetimes
by being less maintainable and more implicit

It is not true that it is more maintainable, I would say it is harder to maintain but being easier to refactor, being easy to refactor is one part, being maintainable but being implicit makes it harder to maintain.

redradist and others added 4 commits July 2, 2020 20:16
Co-authored-by: Ivan Tham <pickfire@riseup.net>
Co-authored-by: Ivan Tham <pickfire@riseup.net>
Update according discussion in comments
Co-authored-by: Ivan Tham <pickfire@riseup.net>
@shepmaster
Copy link
Member

Using a global pandemic as example code is extremely poor judgement and only negatively reflects on this proposal.

@felix91gr
Copy link

@shepmaster I think the covid stuff was @pickfire's thing, but I agree. That's bad taste.

Comment on lines +49 to +60
struct City& {
name: &str,
population: u32,
}

struct State& {
cities: Vec<City&>,
}

struct Country& {
state: Vec<State&>,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks uglier than the original one. I don't quite like the trailing &, no taste IMHO.

@nikomatsakis
Copy link
Contributor

I'm going to go ahead and pre-emptively close this RFC. It's pretty clear from reading the text that it's not in a state to be accepted and still needs significant iteration. Moreover, we're in the process of moving to a new system (the lang-team major change process) that is explicitly intended to capture 'early stage' thinking like this and try to decide if it's a direction we want to pursue, so I think this would be a better fit for that process. Thanks @redradist for the PR!

@redradist
Copy link
Author

@nikomatsakis Have you changed internal processes ? Should either I reopen it or continue discussion in https://internals.rust-lang.org/t/simplification-reference-life-time/12224/31 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.