-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Conversation
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 |
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; |
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. |
struct S<'a, T, U=()> { ... }
let x: S<_, i32> = ...;
fn f(...) -> S<_, i32> { ... } The type signature on |
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 |
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 |
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 |
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. |
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:
|
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 |
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 |
text/0000-argument-lifetimes.md
Outdated
|
||
- 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
I'm wondering if there should be some examples exploring what things look like when they involve
You could name that one
|
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:
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. |
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. |
@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,
) |
@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. |
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 |
You just hit on what what was bothering me that I couldn't quite pin down. |
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:
|
I like this proposal. 👍 I'd prefer if it spelled out the situation around 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. |
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> |
👎 on the case-dependency, as other people already mentioned. Otherwise 👍. So no-vote for now. 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() {} |
Can I just say - I love this proposal. That's all <3 |
text/0000-argument-lifetimes.md
Outdated
that instead of writing | ||
|
||
```rust | ||
fn two_args<'a, 'b>(arg1: &'a Foo, arg2: &'b Bar) -> &'b Baz |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Will fix in revision.
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 |
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. |
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 |
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. |
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:
|
The final comment period is now complete. |
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.) |
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 |
no one commented on my proposal for |
You mean when referred to from outside the same declaration? Yes I think |
@burdges I don't think |
Once experimentation started for a while, how do we quantify whether these changes aren't too dangerous to actually become a thing? |
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> |
@aturon the rendered link is now broken |
As a Rust novice, I can attest the guide proposed in this RFC made lifetimes annotation in structs 'click' for me. 👍 |
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)).
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)).
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.
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. |
NOTE: Updated summary, 2017-09-05
Eliminate the need for separately binding lifetime parameters in
fn
definitions andimpl
headers, so that instead of writing:you can write:
Lint against leaving off lifetime parameters in structs (like
Ref
orIter
), 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:
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.)'_
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