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

RFC: In-band lifetime bindings #2115

Merged
merged 8 commits into from Sep 12, 2017

Conversation

@aturon
Member

aturon commented Aug 17, 2017

NOTE: Updated summary, 2017-09-05

Eliminate the need for separately binding lifetime parameters in fn definitions and impl headers, so that instead of writing:

fn two_args<'b>(arg1: &Foo, arg2: &'b Bar) -> &'b Baz
fn two_lifetimes<'a, 'b>(arg1: &'a Foo, arg2: &'b Bar) -> &'a Quux<'b>

fn nested_lifetime<'inner>(arg: &&'inner Foo) -> &'inner Bar
fn outer_lifetime<'outer>(arg: &'outer &Foo) -> &'outer Bar

you can write:

fn two_args(arg1: &Foo, arg2: &'b Bar) -> &'b Baz
fn two_lifetimes(arg1: &'a Foo, arg2: &'b Bar) -> &'a Quux<'b>

fn nested_lifetime(arg: &&'inner Foo) -> &'inner Bar
fn outer_lifetime(arg: &'outer &Foo) -> &'outer Bar

Lint against leaving off lifetime parameters in structs (like Ref or Iter), instead nudging people to use explicit lifetimes in this case (but leveraging the other improvements to make it ergonomic to do so).

The changes, in summary, are:

  • A signature is taken to bind any lifetimes it mentions that are not already bound.
  • A style lint checks that lifetimes bound in impl headers are multiple characters long, to reduce potential confusion with lifetimes bound within functions. (There are some additional, less important lints proposed as well.)
  • You can write '_ to explicitly elide a lifetime, and it is deprecated to entirely leave off lifetime arguments for non-& types

This RFC does not introduce any breaking changes.

Rendered

@nikomatsakis

This comment has been minimized.

Contributor

nikomatsakis commented Aug 17, 2017

So, I am in favor of this change in direction -- I feel like the previous efforts that were discussed on the internals board (e.g., Foo<'>) were leading in circles and not arriving at a very satisfying point. This proposal seems to achieve the goals of that effort in a better way.

Interestingly, the initial design of Rust's lifetimes worked exactly in this fashion: They were never explicitly declared, and a lifetime name was always scoped to the outermost binding scope in which it appeared. We scrapped this as part of a general move to make lifetimes more explicit. I think this reformulation addresses many of the concerns that arose in the earlier versions (in particular, that early version of Rust didn't support _ for avoiding names on lifetimes, it didn't require that lifetime parameters be declared on structs, etc).

One thing I am not sure about: I am very much in favor of the convention around capitalizing lifetime names in impls, but I am not sure whether this should be a hard rule or a lint. The compiler itself does not, I believe, need to rely on the capitalization convention -- it is more of a way to avoid "accidental" capture where a throw-away lifetime like 'a is used on the impl and then used (again) in a some distant fn. It seems like making it a lint is more consistent with our general philosophy around case conventions -- though, on the other hand, we tend to be stricter around lifetimes (e.g., disallowing shadowing) precisely because people are less familiar and hence confusion has a higher cost.

@repax

This comment has been minimized.

repax commented Aug 17, 2017

I like a lot in this RFC. I'd also love being able to name lifetimes after arguments:

fn two_args(arg1: &Foo, arg2: &Bar) -> &'arg2 Baz;
@ssokolow

This comment has been minimized.

ssokolow commented Aug 17, 2017

I'm a little concerned about the lowercase vs. uppercase distinction.

It feels like something that would have trouble sticking in the learner's mind. (As evidenced by the fact that I didn't sleep well last night and it's the one big thing in the RFC that I just can't seem to remember as soon as I stop looking at the text of the RFC.)

I wouldn't feel comfortable with it unless there's a lint which would let me learn as I go.

@epdtry

This comment has been minimized.

epdtry commented Aug 18, 2017

_ as a wildcard lifetime interacts badly with default type parameters:

struct S<'a, T, U=()> { ... }
let x: S<_, i32> = ...;
fn f(...) -> S<_, i32> { ... }

The type signature on x must expand to S<'_, _, i32> for backward compatibility. But users of the new syntax would likely prefer the return type of f to expand to S<'_, i32, ()>.

@rpjohnst

This comment has been minimized.

rpjohnst commented Aug 18, 2017

I love this way of introducing lifetimes! It's a lot cleaner and more familiar than reusing parameter names.

I would prefer '_ over _. It makes it easier to recognize as an unnamed lifetime- _ is often used for unnamed variables instead, and this would be the only time it's used for lifetimes.

I'm also not a fan of the case distinction. In languages with similar rules around how type variables are introduced, case is often used to mark concrete types vs type variables. On the other hand, this RFC uses them to mark levels of nesting. Further, there are only two cases we can really use here, while there are at least three levels of nesting- impl, fn, and for.

We already error on shadowing lifetime names, so no existing code would be affected by simply removing the <'a>-style bindings. New/modified code might trip and accidentally restrict a lifetime in an unintended way, but that's already possible when accidentally leaving out <'a>-style declarations.

@nikomatsakis

This comment has been minimized.

Contributor

nikomatsakis commented Aug 18, 2017

_ as a wildcard lifetime interacts badly with default type parameters:

Interesting, yeah -- @aturon and I were discussing whether it was unambiguous, but we didn't think about default type parameters. This might of course be something we can address with epochs -- after all, the goal after this RFC is that one should always include _ for lifetimes -- but it may also be worth thinking about '_ or some other syntax.

I think initially @aturon wanted to just use throw-away names but I wasn't happy with that. In particular, I would like to be able to lint that every lifetime name should be used in more than one place (or else you ought to just use _). I think lints like this are spectacularly effective at preventing typos and thus mitigating the risk of not declaring lifetime names with an explicit binder.

@aturon

This comment has been minimized.

Member

aturon commented Aug 18, 2017

@rpjohnst

We already error on shadowing lifetime names, so no existing code would be affected by simply removing the <'a>-style bindings.

Yes, that's true. But the problem is that the current lint on shadowing would become impossible; if you happened to reuse a name within fn, it'd always refer to the outer impl name (which may be a bit far away in the code). But maybe that particular footgun isn't significant enough to warrant this shift.

What did you think about something like outer('a)?

@nikomatsakis

This comment has been minimized.

Contributor

nikomatsakis commented Aug 18, 2017

Further, there are only two cases we can really use here, while there are at least three levels of nesting- impl, fn, and for.

It may be that it's better to drop this idea of a "impl-naming convention" and introduce it later if we find that there is a need for it. That said, what concerns me most is the idea that some code that is "off your screen" might use the same name as you and you wind up with accidental capture. This seems more likely to arise between impls and function signatures than around the use of fn types, but it could certainly arise in either case. For example you might have a local variable:

fn foo(x: &'a u8) {
  // ... more than a screenful of code here ...
  let x: fn(&'a u8) -> &'a u8 = ...;
}

This case in particular feels like we could likely address it with custom error messages -- since it is guaranteed that either (a) the code doesn't compile this way or (b) who cares, since the type -- while not as general as it could be -- was good enough. In the case of impls, the compilation errors are a bit trickier because they stretch across functions, and if the code compiles you might just wind up with a different API than the one you thought you had. I'm not sure.

@TimNN

This comment has been minimized.

Contributor

TimNN commented Aug 18, 2017

I like the general direction of this RFC, however I have one major concern: Making lifetimes the first and so far only place where rustc attaches semantic meaning to the casing of idents sounds like a very bad idea to me. I also think that the rules regarding lifetime classification are currently underspecified:

  • "It is deprecated to use any lifetime variables that do not begin with a capital letter [in impl headers]": So a lifetime starting with _ would be treated as "lowercase"? What about _0 or _A? How does this interact with the non_ascii_idents feature for languages with no distinction between lowercase & uppercase letters? (Technically the quoted sentence answers all those questions, but I think that section should be more explicit) (Live Edit: I just noticed that you mention languages without case distinction in the drawbacks -- I think they should be given further consideration in this RFC, especially since I think that language specific lifetime classification rules are a very bad idea).
  • Would fn foo<T: 'a>(&'a T) continue to work?
@rpjohnst

This comment has been minimized.

rpjohnst commented Aug 18, 2017

What did you think about something like outer('a)?

So, looking through the projects I work on, most impl blocks seem to use either impl-level parameters, or fn-level parameters, almost exclusively. I don't know how typical that is, but the cases that use primarily impl-level parameters would get a lot more verbose with something like outer('a), for not much gain.

I suspect the error messages for accidental capture wouldn't be that bad anyway- the fact that they would refer to the outer impl block as the definition point for the lifetime should tip you off that you've accidentally shadowed a name.

@TimNN

This comment has been minimized.

Contributor

TimNN commented Aug 18, 2017

What did you think about something like outer('a)?

Just to be sure, how would that be used? Would I actually write fn foo(&self, &'outer('a));? That seems much to cumbersome to me.

Also, since we are discussing syntax, what about using "a1 for impl lifetimes and 'a for inner lifetimes.

1: I'm not sure if I actually want that

- If a lowercase lifetime variable occurs anywhere in the signature, it is
*always bound* by the function (as if it were in the `<>` bindings).
- If an uppercase lifetime variable occurs, it is *always a reference* to a

This comment has been minimized.

@est31

est31 Aug 18, 2017

Contributor

always is a strong word: I guess you want to implement it through an error that can't be turned off?

Generally I'm all for enforcing naming rules more strongly, but I think enforcement should be consistent, so IMO we should add this as warn-by-default lint to the bad_style lint group like the other lints that concern naming styles.

@phaylon

This comment has been minimized.

phaylon commented Aug 18, 2017

I'm wondering if there should be some examples exploring what things look like when they involve 'static. For example:

impl<T> VecIter2<'Vec, 'static, T> { ... }

You could name that one 'STATIC to be more naming consistent:

impl<T> VecIter2<'Vec, 'STATIC, T> { ... }
@jonathandturner

This comment has been minimized.

Contributor

jonathandturner commented Aug 18, 2017

I need to work through this a bit, but it seems like your backreferences idea is closer to what I had originally thought seeing the RFC come in.

With backreferences, instead the proposed:

fn two_args(arg1: &Foo, arg2: &'b Bar) -> &'b Baz

You just write:

fn two_args(arg1: &Foo, arg2: &Bar) -> &'arg2 Baz

To my eyes this solves a couple issues with lifetimes:

  • Just as the proposed solution, I don't have to be confused by generics. Additionally, I can see the relationship up front between the lifetime and where it comes from directly.
  • The relationship reads a little nicer than having stand-in variables
  • Still gives you the "I can skip having to write so many lifetimes" approach

I recognize this might just be the case for this small examples, but I worry though without this things actually get worse.

fn two_args(arg1: &Foo, arg2: &'b Bar) -> &'b Baz

What does 'b come from? It's part of the generics but we've just fabricated the name. It feels a bit.. magical. And may not in the best way.

Contrast to argument-named lifetimes, which I think are less magic. We could just say "a lifetime is bound to the argument name when implied" and that would be that.

For lifetimes that are shared between two arguments, we just reuse the name:

fn two_args(arg1: &Foo, arg2: &'arg1 Bar) -> &'arg1 Baz

Of course, with more advanced stuff, we'd see explicit lifetimes come back, but at least we have a nice complexity slope towards that transition.

@pliniker

This comment has been minimized.

pliniker commented Aug 18, 2017

This resonates strongly. I feel hugely positive about this proposal, including backreferences.

I can't speak to technical details and bikeshedish questions in implementing this rfc but in my use of Rust, my mental model of what lifetime parameters do has just in the past month been challenged and I've found the existing syntax and documentation to be a significant barrier to understanding.

I would recommend all these changes - the simplification of syntax, naming lifetimes after parameter names (I really love the backreferences idea) and the documentation that would read so much more intuitively as a result.

@kennytm

This comment has been minimized.

Member

kennytm commented Aug 18, 2017

@jonathandturner Backreference breaks down as soon as you have two lifetimes in a type (&&X, (&X, &Y), &T<'x>, T<'x, 'y>, ...). The proposed RFC generalizes a lot better, at a little cost of teaching "any unseen 'x denotes a new lifetime".

// std::cell::Ref::clone as a free function
pub fn clone<'a, 'b>(orig: &'a Ref<'b, T>) -> Ref<'b, T>; // ??

// std::fmt::Arguments::new_v1 as a free function
fn new_v1<'a>(pieces: &'a [&'a str], args: &'a [ArgumentV1<'a>]) -> Arguments<'a>; // ??

// core::fmt::builders::debug_tuple_new
pub fn debug_tuple_new<'a, 'b>(fmt: &'a mut fmt::Formatter<'b>, name: &str) -> DebugTuple<'a, 'b>; // ??

// the compiler is crazy.
pub fn check_loans<'a, 'b, 'c, 'tcx>(
    bccx: &BorrowckCtxt<'a, 'tcx>,
    dfcx_loans: &LoanDataFlow<'b, 'tcx>,
    move_data: &move_data::FlowedMoveData<'c, 'tcx>,
    all_loans: &[Loan<'tcx>],
    body: &hir::Body,
)
@jonathandturner

This comment has been minimized.

Contributor

jonathandturner commented Aug 18, 2017

@kennytm - just to be clear I'm not saying only back-references. I'm saying they should be an important part of the design, not an optional part.

@rpjohnst

This comment has been minimized.

rpjohnst commented Aug 18, 2017

I would prefer to avoid backreferences if possible, because they feel like an inconsistency with the full version that we need anyway. They also make it look like ' is some kind of lifetime-of operator, which is misleading and not a direction that really makes sense given that Rust code never talks about concrete lifetimes.

@ssokolow

This comment has been minimized.

ssokolow commented Aug 18, 2017

They also make it look like ' is some kind of lifetime-of operator, which is misleading and not a direction that really makes sense given that Rust code never talks about concrete lifetimes.

You just hit on what what was bothering me that I couldn't quite pin down.

@comex

This comment has been minimized.

comex commented Aug 18, 2017

Interesting. I'm vaguely positive on this proposal, but for the record, note a few cases where multiple levels of lifetime-declaration nesting could show up in the future, making the case distinction not a slam dunk:

  • Closures, especially if syntax is ever added for generic parameters on closures; but even without that, under the new syntax, people may expect to be able to introduce lifetimes in closure arguments like they can in fn arguments.

  • I'd like it to be possible someday for type declarations within fns (and corresponding impl blocks) to reference generic parameters from the fn; this is possible in other languages, and it can reduce boilerplate in various situations. Thus there could be many nested impl blocks, all of which have lifetime parameters in scope.

@skade

This comment has been minimized.

skade commented Aug 18, 2017

I like this proposal. 👍 I'd prefer if it spelled out the situation around 'b: 'a also in the main sections, it only gets mentioned in the "Drawbacks" section.

I personally think that:

fn foo<T>(&'a self, bar: &'b T) where 'b: 'a, T: 'b {

}

Reads much better then

fn foo<'a, 'b: 'a, T>(&'a self, bar: &'b T) {

}

Because

a) It keeps the type parameter list clear of lifetimes, which always felt a bit odd at this place (especially when using the function through the turbofish...)
b) It has a natural flow: "self has the lifetime 'a, bar borrows with the lifetime 'b, where the relationships are as follows..."

I think where clauses are good practice in anything more complex then one trait bound anyways, so I don't think that's a problem.

Would it be possible to handle this without deprecations? Keep the old method of working valid, but switch towards a lint phrased like "this isn't necessary anymore"? This wouldn't be a deprecation per se, even if we go towards warning on the old style, but more of a nudge towards the better future. I know this feels like I'm just replacing words here, but deprecations always give the feel that something wasn't working right, lints towards improvements clearly frame things as improvements.

@StyMaar

This comment has been minimized.

StyMaar commented Aug 18, 2017

I'm really excited to see this RFC because I've always found the lifetime syntax to be clunky and verbose.

I'm quite skeptical about the _ marker though, I don't find it really clear and I don't think it adds much, especially if we decide to add backreferences :

// current proposal
fn iter(&self) -> Iter<_, T>

//without the `_` merker
fn iter(&'self self) -> Iter<'self, T>

//with backreferences, not much more verbose than `_` but more explicit
fn iter(&self) -> Iter<'self, T>
@kennytm

This comment has been minimized.

Member

kennytm commented Aug 18, 2017

👎 on the case-dependency, as other people already mentioned. Otherwise 👍. So no-vote for now.

I hate that I have to write MyStruct<'parent> defining the type, but need to remember to change to MyStruct<'Parent> when implementing a trait for it.

If the purpose of case distinction as a hard requirement is just to tackle the "my screen is too short" problem but not a fundamental typesystem restriction, I'd say just drop it, and instead encourage using more descriptive lifetime names for lifetimes in custom types (e.g. the field's name, 'owner, etc.).


I think '_ is better than _, the latter may be confused for a type parameter (seen as HashMap<_, _> elsewhere).


BTW I suppose the new rules be won't affect closures? Today lifetime elision is not applied to closures.

fn foo<'a>(a: &'a u32, b: &u32) -> &'a u32 {
    let c1 = || -> &'a u32 { a };
    let c2 = |x: &'a u32| -> &'a u32 { x };
    let c3 = |_x: &u32| -> &u32 { b }; // currently these are two different lifetimes
    if *c3(b) < 0 {
        c1()
    } else {
        c2(a)
    }
}

fn main() {}
@ubsan

This comment has been minimized.

Contributor

ubsan commented Aug 18, 2017

Can I just say - I love this proposal.

That's all <3

that instead of writing
```rust
fn two_args<'a, 'b>(arg1: &'a Foo, arg2: &'b Bar) -> &'b Baz

This comment has been minimized.

@pnkfelix

pnkfelix Aug 18, 2017

Member

Why did you write <'a, 'b> here when the 'a has no reason to be named, but leave the lifetime unnamed on &Foo below?

The way you've currently written it makes the impact of the binding site more significant than it needs to be (which might be great for motivating the RFC, but lets not do it on false pretenses...).

It would be more fair IMO to make the above signature: fn two_args<'b>(arg1: &Foo, arg2: &'b Bar) -> &'b Baz.

This comment has been minimized.

@pnkfelix

pnkfelix Aug 18, 2017

Member

Or if you prefer, you could find some other way to incorporate the 'a so that it does have to be named.

For example:

  • Before: fn two_args<'a, 'b: 'a>(arg1: &'a Foo, arg2: &'b Bar) -> &'a Baz<'b>
  • After: fn two_args(arg1: &'a Foo, arg2: &'b Bar) -> &'a Baz<'b>

This comment has been minimized.

@aturon

aturon Aug 18, 2017

Member

Good catch! Will fix in revision.

@djc

This comment has been minimized.

Contributor

djc commented Aug 18, 2017

I actually like the "lifetimes from types and impls are uppercased", since I think it will help finding the origin of the lifetime bound where they occur.

I would be very much in favor of backreferences that would allow us to write:

fn myfun(&self, foo: &Foo) -> &'self Bar

Since this will actually make it much more attractive to use readable names for lifetimes (since now the "declaration" can be omitted.

The RFC could be clearer on how the different parts of this proposal interact with checkpoints, maybe that could be clarified? Like, "type lifetimes with non-capitalized names will be deprecated in checkpoint 2015, and disallowed in checkpoint 2018".

I don't know the details of elision works today, but would it be an option to adopt this proposal and deprecate elision as it currently is, but then allow unspecified lifetimes in the return type to be omitted if-and-only-if there is a &self in play, as a simpler, easier to predict and remember version of elision that only works on methods?

@RalfJung

This comment has been minimized.

Member

RalfJung commented Sep 7, 2017

@nikomatsakis

This comment has been minimized.

Contributor

nikomatsakis commented Sep 8, 2017

I am torn on the question of the lint. I feel like I really do want some convention to distinguish between impls and not, but I find it a bit hard to decide what I think it ought to be. =)

I am basing a lot of my intuition on the compiler's use of lifetimes, which afaik is one of the most thorough uses that exists in the wild. Our pattern is pretty clear: there are various "well known" lifetime names that appear all over the place. Notably, 'tcx and 'gcx. Each refers to the lifetime of a particular arena ('tcx is the "local" arena for a particular type-inferencing session, 'gcx is the "global arena" that persists for the entire compilation). These are typically bound at the impl level, e.g. in code like this:

// "type context"
type TyCtxt<'a, 'gcx, 'tcx> = &'a TyCtxtData<'gcx, 'tcx>; // alias we use *everywhere*

impl<'a, 'gcx, 'tcx> TyCtxt<'a, 'gcx, 'tcx> {
    fn do_something(self, data: Ty<'tcx>) -> Results<'tcx> { ... }
}

struct SomeOtherContext<'a, 'gcx, 'tcx: 'tcx> {
   tcx: TyCtxt<'a, 'gcx, 'tcx>,
   ...,
}

impl<'a, 'gcx, 'tcx> SomeOtherContext<'a, 'gcx, 'tcx> {

In the current system, binding at the impl level makes this whole thing feasible, because 'tcx and 'gcx basically appear on every other line of the compiler. If you had to declare them on every function with <'gcx, 'tcx> on every function, you'd go mad (I think @eddyb almost did, when putting in the current system).

The interesting thing here is that I think it's actually not so important whether 'tcx or 'gcx is bound at the impl or the function level. If you see those names, you know what they mean. They are globally relevant. In other words, I think I would prefer to write things like this:

impl SomeStruct<'tcx> {
  fn compute(self, ty: Ty<'tcx>) -> Ty<'tcx> { .. }
}

fn some_free_fn(some_struct: SomeStruct<'tcx>, ty: Ty<'tcx>) -> Ty<'tcx> { .. }

Now, it'd be ok to change to 'Tcx when the name is bound on an impl, but it feels like kind of a distraction. I want to be using the same name in both of those examples.

On the other hand, the compiler's current conventions around this final lifetime -- 'a -- seem problematic. Let me clarify its role: when you have a type like SomeOtherContext<'a, 'gcx, 'tcx>, the 'a there represents the lifetime of the particular reference to the type context that it contains. This tends to be a kind of "as short as it can be" lifetime, usually equal to the type in which SomeOtherContext is used, and used to refer to random data that just happens to outlive SomeOtherContext but is not necessarily stored in any particular arena.

The good part about using the name 'a here is that this is not a "well-known" lifetime -- it doesn't name any particular thing, unlike 'tcx or 'gcx. But on the other hand it causes problems precisely because of the kinds of shadowing we are wary of here -- it's easy to have an impl that binds 'a but then accidentally add a function that uses it for some other purpose (fn some_method<'a>(...)). Today, this causes a shadowing error -- and I definitely think we need to establish conventions to remove the possibility of this conflict.

Now, my first thought was that the best thing here would be for us to pick another conventional name for this lifetime. For example, we could call it 'cx, or 'pass, or something else. Then we would reserve the single-letter names for "throw-away" lifetimes that happen to appear in individual function signatures. I think that this intution is roughly what @aturon was driving at in the current proposal.

I think that if people used such a convention wisely -- i.e., avoiding throw-away names in general, but especially in impls -- then the chance of conflict is indeed very low.

Nonetheless, I am sympathetic to @glaebhoerl's point that it'd be nice to have somewhat stricter convention, that helps draw attention (particularly in methods) to lifetimes bound on impls. I'm not sure how to square that with my feeling that, when you do have "significant" lifetimes like 'gcx and 'tcx, having to change their name based on where they are bound is actually counterproductive.

It'd be worth exploring other scenarios where impls are bound on types (for example, this is common in iterators, etc), and try to tease out if there are other patterns that are relevant. I do strongly suspect that just having a convention there would also be very helpful. For example, when you are defining an iterator that has a reference to a collection, instead of using 'a for that lifetime, perhaps using 'collection (or 'coll if that's too long) would be better.

@glaebhoerl

This comment has been minimized.

Contributor

glaebhoerl commented Sep 8, 2017

@nikomatsakis We could allow uppercase lifetimes on free-standing functions, I think -- there's no ambiguity or potential for conflict there. Just not on methods inside of impl blocks. (edit: so to be clear, you could still have globally consistent names (IINM), they'd just be the uppercase ones)

@withoutboats

This comment has been minimized.

Contributor

withoutboats commented Sep 8, 2017

I think we should implement this, use it on nightly, and see what conventions or lints make sense arrising from that experience. This conversation about how to distinguish function level from impl level lifetimes seems to me like a prime example of how our processes have become far too "waterfall" - its one thing to be aware of this concern, and know we may need a way to mitigate it, but its another thing to have a lengthy debate about what the best mitigation strategy is without getting any practical experience of the situation.

@burdges

This comment has been minimized.

burdges commented Sep 9, 2017

We could permit ambiguity only on multi-letter lifetimes, under the assumption that their name corresponds to come meaningful convention in the code base, but require that single letter implicitly bound lifetimes were upper or lower case corresponding to whether they were implicitly bound by the impl or fn.

@TimNN

This comment has been minimized.

Contributor

TimNN commented Sep 9, 2017

Since the topic of experimenting has been brought up, has anyone considered making this an experimental RFC? The only thing that would change is requiring another RFC before stabilising the feature, which seems reasonable given that there are still concerns, which I don't think can really be resolved without some experimentation.

@vitiral

This comment has been minimized.

vitiral commented Sep 9, 2017

I would just like to point out that the whole problem here is that this RFC is essentially auto declaring variable names (for a special kind of variable: the lifetime).

Until this RFC, you had to declare your lifetime and their "scope" with the fn foo<'a, 'b, ...>(a: &'a [u8]...) syntax. This is trying to remove that boilerplate by auto-inferring the names. Unfortunately, this makes the scope of lifetimes no longer clear.

It's really rather annoying. It feels like this problem should be solvable. I can think of a few possible solutions, although I like none of them:

  • lifetimes attached to structs must be capitalized (and must not be capitalized for methods) for auto-inferring to kick in. This was already suggested
  • add some kind of sigil to (i.e. @) to denote that the lifetime is declared elsewhere. 'foo is always local, but '@foo is always defined in the impl header. I am pretty opposed to adding new sigils
  • lint to require 'self::<lt> for non-local lifetimes. This is at least extremely clear.
impl<'abc> for MyStruct<'abc> {
    fn foo() -> &'self::abc [u8] {}
}
@rfcbot

This comment has been minimized.

rfcbot commented Sep 10, 2017

The final comment period is now complete.

@nikomatsakis

This comment has been minimized.

Contributor

nikomatsakis commented Sep 11, 2017

@glaebhoerl

We could allow uppercase lifetimes on free-standing functions, I think -- there's no ambiguity or potential for conflict there. Just not on methods inside of impl blocks. (edit: so to be clear, you could still have globally consistent names (IINM), they'd just be the uppercase ones)

I considered this. I held back on suggesting it because the "globally consistent names" in some cases would have to be lower-cases:

impl MyType { // no lifetime parameters
    fn process(&self, tcx: TyCtxt<'tcx>) -> Ty<'tcx> { .. } // can't be `'Tcx` here
}

I feel like in practice I would prefer a lint that says "names used across scopes must have more than one letter", but mostly I agree with @withoutboats that we should work this out after gaining some more experience "live":

[@withoutboats]: This conversation about how to distinguish function level from impl level lifetimes seems to me like a prime example of how our processes have become far too "waterfall"

Indeed.

--

@TimNN

Since the topic of experimenting has been brought up, has anyone considered making this an experimental RFC?

I'm not opposed, but it seems unnecessary to me personally. I feel like we could leave an official "Unresolved Question" of "what naming convention would be best to distinguish the scopes", so that we are sure to revisit the question prior to stabilizing.

That is, to me, an eRFC is needed when there are major pieces of the design missing. For example, in the generators RFC, it was unclear what syntax we should use, whether a special trait (Generator) was needed or whether we could leverage FnMut, etc. Here, it feels like we know we want a lint, we just don't know precisely what it should do.

(Though, to be honest, I think I'd like to merge the RFC + eRFC process.)

@aturon aturon referenced this pull request Sep 12, 2017

Open

Tracking issue for RFC 2115: In-band lifetime bindings #44524

3 of 9 tasks complete
@aturon

This comment has been minimized.

Member

aturon commented Sep 12, 2017

Thanks, all, for the thorough discussion! This RFC has now been merged! Tracking issue

During FCP, most discussion centered on concerns about the right convention (if any) to lint enforce for avoiding accidental clashes between impl headers and the items they contain. As @withoutboats said, however, it seems best to settle this particular question with some experience with the feature in hand; it's difficult to judge hypothetically. This has been added as an explicit unresolved question on the tracking issue, which must be revisited prior to any stabilization.

@aturon aturon merged commit 1be52cd into rust-lang:master Sep 12, 2017

@vitiral

This comment has been minimized.

vitiral commented Sep 12, 2017

no one commented on my proposal for 'self::lifetime_name ☹️

@burdges

This comment has been minimized.

burdges commented Sep 12, 2017

You mean when referred to from outside the same declaration? Yes I think 'self::lifetime_name would work for the impl, along with 'fn::lifetime_name for the fn or maybe 'fn_name::lifetime_name. I do think that syntax is unambiguous.

@vitiral

This comment has been minimized.

vitiral commented Sep 13, 2017

@burdges I don't think 'fn:: is necessary, since there are no global lifetimes except for 'static. A function declares all it's lifetimes along with the variable names (under this RFC)

@nox

This comment has been minimized.

Contributor

nox commented Sep 13, 2017

Once experimentation started for a while, how do we quantify whether these changes aren't too dangerous to actually become a thing?

@dhardy

This comment has been minimized.

Contributor

dhardy commented Sep 13, 2017

Is this bit of the RFC wrong (outdated)? It mentions further down that the "backreference" syntax is not preferred.

fn elided(&self) -> &str
fn two_args(arg1: &Foo, arg2: &Bar) -> &'arg2 Baz
fn two_lifetimes(arg1: &Foo, arg2: &Bar) -> &'arg1 Quux<'arg2>
@vitiral

This comment has been minimized.

vitiral commented Sep 13, 2017

@aturon the rendered link is now broken

@bmisiak

This comment has been minimized.

bmisiak commented Apr 16, 2018

As a Rust novice, I can attest the guide proposed in this RFC made lifetimes annotation in structs 'click' for me. 👍

@rpjohnst rpjohnst referenced this pull request May 5, 2018

Closed

RFC: `throw` expressions #2426

jturner314 added a commit to jturner314/rust-rfcs that referenced this pull request Jun 23, 2018

Fix in-band lifetimes example with backreferences
Backreferences are listed as a "possible extension or alternative" in
RFC rust-lang#2115, so the examples should not include them. This is further
reinforced by commit c20ea6d, which
appears to have been intended to remove all instances of
backreferences in the examples, but missed these. [@dhardy also
observed this
issue](rust-lang#2115 (comment)).

jturner314 added a commit to jturner314/rust-rfcs that referenced this pull request Jun 23, 2018

Fix in-band lifetimes example with backreferences
Backreferences are listed as a "possible extension or alternative" in
RFC rust-lang#2115, so the examples should not include them. This is further
reinforced by commit c20ea6d, which
appears to have been intended to remove all instances of
backreferences in the examples, but missed these. [@dhardy also
observed this issue]
(rust-lang#2115 (comment)).

jturner314 added a commit to jturner314/rust-rfcs that referenced this pull request Jun 23, 2018

Fix in-band lifetimes example with backreferences
Backreferences are listed as a "possible extension or alternative" in
RFC rust-lang#2115, so the examples should not include them. This is further
reinforced by commit c20ea6d, which
appears to have been intended to remove all instances of
backreferences in the examples, but missed these.
@warlord500

This comment has been minimized.

warlord500 commented Jun 27, 2018

I would definitely not want the part where you don't have to declare lifetimes because of added inconsistency in the language and ambiguity of where life time parameters come from.

I also dont like back reference lifetime, it adds a really weird special case to lifetime.
making lifetimes even more confusing because of the rules of describing bounds on them.
where the concept behind them is rather simple

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment