Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upRFC: In-band lifetime bindings #2115
Conversation
aturon
added
Ergonomics Initiative
T-lang
labels
Aug 17, 2017
aturon
self-assigned this
Aug 17, 2017
This comment has been minimized.
This comment has been minimized.
|
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., 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 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 |
This comment has been minimized.
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; |
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
epdtry
commented
Aug 18, 2017
|
struct S<'a, T, U=()> { ... }
let x: S<_, i32> = ...;
fn f(...) -> S<_, i32> { ... }The type signature on |
This comment has been minimized.
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 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- We already error on shadowing lifetime names, so no existing code would be affected by simply removing the |
This comment has been minimized.
This comment has been minimized.
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 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 |
This comment has been minimized.
This comment has been minimized.
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 What did you think about something like |
This comment has been minimized.
This comment has been minimized.
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 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. |
This comment has been minimized.
This comment has been minimized.
|
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:
|
This comment has been minimized.
This comment has been minimized.
rpjohnst
commented
Aug 18, 2017
So, looking through the projects I work on, most I suspect the error messages for accidental capture wouldn't be that bad anyway- the fact that they would refer to the outer |
This comment has been minimized.
This comment has been minimized.
Just to be sure, how would that be used? Would I actually write Also, since we are discussing syntax, what about using 1: I'm not sure if I actually want that |
est31
reviewed
Aug 18, 2017
|
|
||
| - 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.
This comment has been minimized.
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.
This comment has been minimized.
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
You could name that one
|
This comment has been minimized.
This comment has been minimized.
|
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 BazYou just write: fn two_args(arg1: &Foo, arg2: &Bar) -> &'arg2 BazTo my eyes this solves a couple issues with lifetimes:
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 BazWhat 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 BazOf course, with more advanced stuff, we'd see explicit lifetimes come back, but at least we have a nice complexity slope towards that transition. |
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
|
@jonathandturner Backreference breaks down as soon as you have two lifetimes in a type ( // 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,
) |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
ssokolow
commented
Aug 18, 2017
You just hit on what what was bothering me that I couldn't quite pin down. |
This comment has been minimized.
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:
|
This comment has been minimized.
This comment has been minimized.
|
I like this proposal. 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...) 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. |
This comment has been minimized.
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 // 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> |
This comment has been minimized.
This comment has been minimized.
|
I hate that I have to write 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, I think 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() {} |
This comment has been minimized.
This comment has been minimized.
|
Can I just say - I love this proposal. That's all <3 |
pnkfelix
reviewed
Aug 18, 2017
| that instead of writing | ||
|
|
||
| ```rust | ||
| fn two_args<'a, 'b>(arg1: &'a Foo, arg2: &'b Bar) -> &'b Baz |
This comment has been minimized.
This comment has been minimized.
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.
This comment has been minimized.
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.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
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 BarSince 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 |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
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 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:
|
This comment has been minimized.
This comment has been minimized.
rfcbot
commented
Sep 10, 2017
|
The final comment period is now complete. |
nikomatsakis
referenced this pull request
Sep 11, 2017
Open
region errors: suggest new signature #44506
This comment has been minimized.
This comment has been minimized.
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":
Indeed. --
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 ( (Though, to be honest, I think I'd like to merge the RFC + eRFC process.) |
aturon
referenced this pull request
Sep 12, 2017
Open
Tracking issue for RFC 2115: In-band lifetime bindings #44524
This comment has been minimized.
This comment has been minimized.
|
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 |
aturon
merged commit 1be52cd
into
rust-lang:master
Sep 12, 2017
This comment has been minimized.
This comment has been minimized.
vitiral
commented
Sep 12, 2017
|
no one commented on my proposal for |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Sep 12, 2017
|
You mean when referred to from outside the same declaration? Yes I think |
This comment has been minimized.
This comment has been minimized.
vitiral
commented
Sep 13, 2017
|
@burdges I don't think |
This comment has been minimized.
This comment has been minimized.
|
Once experimentation started for a while, how do we quantify whether these changes aren't too dangerous to actually become a thing? |
This comment has been minimized.
This comment has been minimized.
|
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> |
This comment has been minimized.
This comment has been minimized.
vitiral
commented
Sep 13, 2017
|
@aturon the rendered link is now broken |
PieterPenninckx
referenced this pull request
Sep 15, 2017
Open
tracking issue for lifetime inference error work (E0495) #42516
This comment has been minimized.
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. |
jturner314
added a commit
to jturner314/rust-rfcs
that referenced
this pull request
Jun 23, 2018
jturner314
added a commit
to jturner314/rust-rfcs
that referenced
this pull request
Jun 23, 2018
jturner314
added a commit
to jturner314/rust-rfcs
that referenced
this pull request
Jun 23, 2018
jturner314
referenced this pull request
Jun 23, 2018
Closed
Fix in-band lifetimes example with backreferences #2485
This comment has been minimized.
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. |
aturon commentedAug 17, 2017
•
edited by mbrubeck
NOTE: Updated summary, 2017-09-05
Eliminate the need for separately binding lifetime parameters in
fndefinitions andimplheaders, so that instead of writing:you can write:
Lint against leaving off lifetime parameters in structs (like
ReforIter), 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:
implheaders are multiple characters long, to reduce potential confusion with lifetimes bound within functions. (There are some additional, less important lints proposed as well.)'_to explicitly elide a lifetime, and it is deprecated to entirely leave off lifetime arguments for non-&typesThis RFC does not introduce any breaking changes.
Rendered