From 115fa820847e766da7bb245e854185ed59dd33e2 Mon Sep 17 00:00:00 2001 From: Aaron Turon Date: Thu, 17 Aug 2017 16:05:39 -0700 Subject: [PATCH 1/8] RFC: Argument-bound lifetimes --- text/0000-argument-lifetimes.md | 492 ++++++++++++++++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 text/0000-argument-lifetimes.md diff --git a/text/0000-argument-lifetimes.md b/text/0000-argument-lifetimes.md new file mode 100644 index 00000000000..124756200eb --- /dev/null +++ b/text/0000-argument-lifetimes.md @@ -0,0 +1,492 @@ +- Feature Name: argument_lifetimes +- Start Date: 2017-08-17 +- RFC PR: (leave this empty) +- Rust Issue: (leave this empty) + +# Summary +[summary]: #summary + +Improves the clarity, ergonomics, and learnability around explicit lifetimes, so +that instead of writing + +```rust +fn two_args<'a, 'b>(arg1: &'a Foo, arg2: &'b Bar) -> &'b Baz +``` + +you can write: + +```rust +fn two_args(arg1: &Foo, arg2: &'b Bar) -> &'b Baz +``` + +In particular, this RFC completely removes the need for listing lifetime +parameters, instead binding them "in-place" (but with absolute clarity about +*when* this binding is happening): + +```rust +fn named_lifetime(arg: &'inner Foo) -> &'inner Bar +fn nested_lifetime(arg: &&'inner Foo) -> &'inner Bar +``` + +It also proposes linting 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). + +# Motivation +[motivation]: #motivation + +Today's system of lifetime elision has a kind of "cliff". In cases where elision +applies (because the necessary lifetimes are clear from the signature), you +don't need to write anything: + +```rust +fn one_arg(arg: &Foo) -> &Baz +``` + +But the moment that lifetimes need to be disambiguated, you suddenly have to +introduce a named lifetime parameter and refer to it throughout, which generally +requires changing three parts of the signature: + +```rust +fn two_args<'a, 'b>(arg1: &'a Foo, arg2: &'b Bar) -> &'b Baz +``` + +In much idiomatic Rust code, these lifetime parameters are given meaningless +names like `'a`, because they're serving merely to tie pieces of the signature +together. This habit indicates a kind of design smell: we're forcing programmers +to conjure up and name a parameter whose identity doesn't matter to them. + +Moreover, when reading a signature involving lifetime parameters, you need to +scan the whole thing, keeping `'a` and `'b` in your head, to understand the +pattern of borrowing at play. + +These concerns are just a papercut for advanced Rust users, but they also +present a cliff in the learning curve, one affecting the most novel and +difficult to learn part of Rust. In particular, when first explaining borrowing, +we can say that `&` means "borrowed" and that borrowed values coming out of a +function must come from borrowed values in its input: + +```rust +fn accessor(&self) -> &Foo +``` + +It's then not too surprising that when there are multiple input borrows, you +need to disambiguate which one you're borrowing from. But to learn how to do so, +you must learn not only lifetimes, but also the system of lifetime +parameterization and the subtle way you use it to tie lifetimes together. In +the next section, I'll show how this RFC provides a gentler learning curve +around lifetimes and disambiguation. + +Another point of confusion for newcomers and old hands alike is the fact that +you can leave lifetimes off when using types: + +```rust +struct Iter<'a> { ... } + +impl SomeType { + // Iter here implicitly takes the lifetime from &self + fn iter(&self) -> Iter { ... } +``` + +As detailed in the [ergonomics initiative blog post], this bit of lifetime +elision is considered a mistake: it makes it difficult to see at a glance that +borrowing is occurring, especially if you're unfamiliar with the types involved. +This RFC proposes some steps to rectify this situation without regressing +ergonomics significantly. + +[ergonomics initiative blog post]: https://blog.rust-lang.org/2017/03/02/lang-ergonomics.html + +In short, this RFC seeks to improve the lifetime story for existing and new +users by simultaneously improving clarity and ergonomics. In practice it should +reduce the total occurrences of `<`, `>` and `'a` in signatures, while +*increasing* the overall clarity and explicitness of the lifetime system. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +*Note: this is a **sketch** of what it might look like to teach someone +lifetimes given this RFC**. + +## Introducing references and borrowing + +*Assume that ownership has already been introduced, but not yet borrowing*. + +While ownership is important in Rust, it's not very expressive or convenient by +itself; it's quite common to want to "lend" a value to a function you're +calling, without permanently relinquishing ownership of it. + +Rust provides support for this kind of temporary lending through *references* +`&T`, which signify *a temporarily borrowed value of type `T`*. So, for example, +you can write: + +```rust +fn print_vec(vec: &Vec) { + for i in vec { + println!("{}", i); + } +} +``` + +and you designate lending by writing an `&` on the callee side: + +```rust +print_vec(&my_vec) +``` + +This borrow of `my_vec` lasts only for the duration of the `print_vec` call. + +*Imagine more explanation here...* + +## Functions that return borrowed data + +So far we've only seen functions that *consume* borrowed data; what about +producing it? + +In general, borrowed data is always borrowed *from something*. And that thing +must always be available for longer than the borrow is. When a function returns, +its stack frame is destroyed, which means that any borrowed data it returns must +come from outside of its stack frame. + +The most typical case is producing new borrowed data from already-borrowed +data. For example, consider a "getter" method: + +```rust +struct MyStruct { + field1: Foo, + field2: Bar, +} + +impl MyStruct { + fn get_field1(&self) -> &Foo { + &self.field1 + } +} +``` + +Here we're making what looks like a "fresh" borrow, it's "derived" from the +existing borrow of `self`, and hence fine to return back to our caller; the +actual `MyStruct` value must live outside our stack frame anyway. + +### Pinpointing borrows with lifetimes + +For Rust to guarantee safety, it needs to track the *lifetime* of each loan, +which says *for what portion of code the loan is valid*. + +In particular, each `&` type also has an associated lifetime---but you can +usually leave it off. The reason is that a lot of code works like the getter +example above, where you're returning borrowed data which could only have come +from the borrowed data you took in. Thus, in `get_field1` the lifetime for +`&self` and for `&Foo` are assumed to be the same. + +Rust is conservative about leaving lifetimes off, though: if there's any +ambiguity, you need to say explicitly state the relationships between the +loans. So for example, the following function signature is *not* accepted: + +```rust +fn select(data: &Data, params: &Params) -> &Item; +``` + +Rust cannot tell how long the resulting borrow of `Item` is valid for; it can't +deduce its lifetime. Instead, you need to connect it to one or both of the input +borrows: + +```rust +fn select(data: &'data Data, params: &Params) -> &'data Item; +fn select(data: &'both Data, params: &'both Params) -> &'both Item; +``` + +This notation lets you *name* the lifetime associated with a borrow and refer to +it later: + +- In the first variant, we name the `Data` borrow lifetime `'data`, and make +clear that the returned `Item` borrow is valid for the same lifetime. + +- In the second variant, we give *both* input lifetimes the *same* name `'both`, +which is a way of asking the compiler to determine their "intersection" +(i.e. the period for which both of the loans are active); we then say the +returned `Item` borrow is valid for that period (which means it may incorporate +data from both of the input borrows). + +You can use any names you like when introducing lifetimes (which always start +with `'`), but are encouraged to make them meaningful, e.g. by using the same +name as the parameter. + +## `struct`s and lifetimes + +Sometimes you need to build data types that contain borrowed data. Since those +types can then be used in many contexts, you can't say in advance what the +lifetime of those borrows will be. Instead, you must take it as a parameter: + +```rust +struct VecIter<'vec, T> { + vec: &'vec Vec, + index: usize, +} +``` + +Here we're defining a type for iterating over a vector, without requiring +*ownership* of that vector. To do so, we store a *borrow* of the vector. But +because our new `VecIter` struct contains borrowed data, it needs to surface +that fact, and the lifetime connected with it. It does so by taking an explicit +`'vec` parameter for the relevant lifetime, and using it within. + +When using this struct, you can apply explicitly-named lifetimes as usual: + +```rust +impl Vec { + fn iter(&'vec self) -> VecIter<'vec, T> { ... } +} +``` + +However, in cases like this example, we would normally be able to leave off the +lifetime with `&`, since there's only one source of data we could be borrowing +from. We can do something similar with structs: + +```rust +impl Vec { + fn iter(&self) -> VecIter<_, T> { ... } +} +``` + +The `_` marker makes clear to the reader that *borrowing is happening*, which +might not otherwise be clear. + +## `impl` blocks and lifetimes + +When writing an `impl` block for a structure that takes a lifetime parameter, +you can give that parameter a name, but it must begin with an uppercase letter: + +```rust +impl VecIter<'Vec, T> { ... } +``` + +The reason for this distinction is so that there's no potential for confusion +with lifetime names introduced by methods within the `impl` block. In other +words, it makes clear whether the method is *introducing* a new lifetime +(lowercase), or *referring* to the one in the `impl` header (uppercase): + +```rust +impl VecIter<'Vec, T> { + fn foo(&self) -> &'Vec T { ... } + fn bar(&self, arg: &'arg Bar) -> &'arg Bar { ... } + + // these two are the same: + fn baz(&self) -> &T { ... } + fn baz(&'self self) -> &'self T { ... } +} +``` + +If the type's lifetime is not relevant, you can leave it off using `_`: + +```rust +impl VecIter<_, T> { ... } +``` + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +## Lifetimes in `impl` headers + +When writing an `impl` header, it is deprecated to bind a lifetime parameter +within the generics specification (e.g. `impl<'a>`). It is also deprecated to +use any lifetime variables that do not begin with a capital letter (which is +needed to clearly distinguish lifetimes that are bound in the `impl` header from +those bound in signatures). + +Instead the `impl` header can mention lifetimes without adding them as +generics. **These lifetimes *always* become parameters of the `impl`**--after +all, **no lifetime variables can possibly be in scope already**. In other words, +the lifetime parameters of an `impl` are taken to be the set of lifetime +variables it uses. + +Thus, where today you would write: + +```rust +impl<'a> Iterator for MyIter<'a> { ... } +impl<'a, 'b> SomeTrait<'a> for SomeType<'a, 'b> { ... } +``` + +tomorrow you would write: + +```rust +impl Iterator for MyIter<'A> { ... } +impl SomeTrait<'A> for SomeType<'A, 'B> { ... } +``` + +## Lifetimes in `fn` signatures + +When writing a `fn` declaration, it is deprecated to bind a lifetime parameter +within the generics specification (e.g. `fn foo<'a>(arg: &'a str)`). + +Instead: + +- 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 + lifetime bound by the immediate containing `impl` header, and thus must occur + in that header. +- It is illegal for the return type to mention any lowercase lifetime variables + that do not occur in at least one argument. +- As with today's elision rules, lifetimes that appear *only* within `Fn`-style + bounds or trait object types are bound in higher-rank form (i.e., as if you'd + written them using a `for<'a>`). + +Thus, where today you would write: + +```rust +fn elided(&self) -> &str; +fn two_args<'a, 'b>(arg1: &'a Foo, arg2: &'b Bar) -> &'b Baz; + +impl<'a> MyStruct<'a> { + fn foo(&self) -> &'a str; + fn bar<'b>(&self, arg: &'b str) -> &'b str; +} + +fn take_fn<'a>(x: &'a u32, y: for<'b> fn(&'a u32, &'b u32, &'b u32)) +``` + +tomorrow you would write: + +```rust +fn elided(&self) -> &str; +fn two_args(arg1: &Foo, arg2: &'b Bar) -> &'b Baz; + +impl MyStruct<'A> { + fn foo(&self) -> &'A str; + fn bar(&self, arg: &'b str) -> &'b str; +} + +fn take_fn(x: &'a u32, y: fn(&'a u32, &'b u32, &'b u32)); +``` + +## The wildcard lifetime + +When referring to a type (other than `&`/`&mut`) that requires lifetime +arguments, it is deprecated to leave off those parameters. + +Instead, you can write a `_` for the parameters, rather than giving a lifetime +name, which will have identical behavior to leaving them off today. + +Thus, where today you would write: + +```rust +fn foo(&self) -> Ref +fn iter(&self) -> Iter +``` + +tomorrow you would write: + +```rust +fn foo(&self) -> Ref<_, SomeType> +fn iter(&self) -> Iter<_, T> +``` + +# Drawbacks +[drawbacks]: #drawbacks + +The deprecations here involve some amount of churn (largely in the form of +deleting lifetimes from `<>` blocks, but also sometimes changing their +case). Users exercise a lot of control over when they address that churn and can +do so incrementally. Moreover, we can and should consider addressing this with a +`rustfix`, which should be easy and highly reliable. + +The fact that lifetime parameters are not bound in an out-of-band way is +somewhat unusual and might be confusing---but then, so are lifetime parameters! +Putting the bindings out of band buys us very little, as argued in the next +section. + +Introducing a case distinction for lifetime names is a bit clunky, and of course +not all spoken languages *have* a case distinction (but for those, another +convention could be applied). + +Requiring a `_` rather than being able to leave off lifetimes altogether may be +a slight decrease in ergonomics in some cases. + +Cases where you could write `fn foo<'a, 'b: 'a>(...)` now need the `'b: 'a` to +be given in a `where` clause, which might be slightly more verbose. These are +relatively rare, though, due to our type well-formedness rule. + +Otherwise, it's a bit hard to see drawbacks here: nothings is made more explicit +or harder to determine, since the binding structure continues to be completely +unambiguous; ergonomics and, arguably, learnability both improve. And +signatures become less noisy and easier to read. + +# Rationale and Alternatives +[alternatives]: #alternatives + +## Core rationale + +The key insight of the proposed design is that out-of-band bindings for lifetime +parameters is buying us very little today: + +- For free functions, it's completely unnecessary; the only lifetime "in scope" + is `'static`, so everything else *must* be a parameter. +- For functions within `impl` blocks, it is solely serving the purpose of + distinguishing between lifetimes bound by the `impl` header and those bounds + by the `fn`. + +**Thus, if we have some other way of distinguishing between `impl`-bound and +`fn`-bound lifetimes, binding lists for lifetimes are completely redundant**; we +can eliminate them without introducing any ambiguity. + +This RFC proposes to make the distinction through case; uppercase lifetimes are +already linted against today. However, we could instead distinguish it purely at +the use-site, for example by writing `outer('a)` or some such to refer to the +`impl` block bindings. In any case, having immediate visual clarity in `fn` +declarations as to whether a lifetime is coming from the `fn` or `impl` block is +a nice benefit. + +## Possible extension: "backreferences" + +This RFC was written with a particular extension in mind: allowing you to refer +to elided lifetimes through their parameter name, like so: + +```rust +fn scramble(&self, arg: &Foo) -> &'self Bar +``` + +Here, we are referring to `'self` in the return type without any references in +the argument---something that the RFC specifies will be an error. The idea is +that each parameter that involves a single, elided lifetime will be understood +to *bind* a lifetime using that parameter's name. + +For the sake of conservative, incremental progress, this RFC punts on the +extension, but tries to leave the door open to it. That will allow us to gather +more data before deciding on this additional step. (To be fully +forward-compatible, we probably need to make it an error to use the name of an +argument as a lifetime *unless* it is used in that argument as well.) + +Alternatively, we could consider including this extension up front, perhaps as a +feature gate, so that we can gain experience more quickly, but make +stabilization decisions separately. + +## Alternatives + +We could consider *only* allowing "backreferences", and otherwise keeping +binding as-is. However, that would forgo the benefits of eliminating out-of-band +binding, which would still be needed in some cases. + +We could consider using this as an opportunity to eliminate `'` altogether, by +tying these improvements to a new way of providing lifetimes, e.g. `&ref(x) T`. + +The [internals thread] on this topic covers a wide array of syntactic options +for leaving off a struct lifetime (which is `_` in this RFC), including: `'_`, +`&`, `ref`. The choice of `_` was informed by a few things: + +- It's short, evocative, and not too visually jarring. +- In signatures, it *always* means "lifetime elided", so there's a clear signal + re: borrowing. + +[internals thread]: (https://internals.rust-lang.org/t/lang-team-minutes-elision-2-0/5182) + +As mentioned above, we could consider alternatives to the case distinction in +lifetime variables, instead using something like `outer('a)` to refer to +lifetimes from an `impl` header. + +# Unresolved questions +[unresolved]: #unresolved-questions + +- Should we include the "backreference" extension up front? It seems very likely + to be desired in this setup. + +- Should we go further and eliminate the need for `for<'a>` notation as well? From a8fa5ad9b56e06f6626446d9925b3fe16815f5d9 Mon Sep 17 00:00:00 2001 From: Aaron Turon Date: Fri, 18 Aug 2017 14:22:57 -0700 Subject: [PATCH 2/8] Updates from first round of feedback --- text/0000-argument-lifetimes.md | 189 +++++++++++++++----------------- 1 file changed, 88 insertions(+), 101 deletions(-) diff --git a/text/0000-argument-lifetimes.md b/text/0000-argument-lifetimes.md index 124756200eb..2b290bc97e3 100644 --- a/text/0000-argument-lifetimes.md +++ b/text/0000-argument-lifetimes.md @@ -10,28 +10,44 @@ Improves the clarity, ergonomics, and learnability around explicit lifetimes, so that instead of writing ```rust -fn two_args<'a, 'b>(arg1: &'a Foo, arg2: &'b Bar) -> &'b Baz +fn two_args<'b>(arg1: &Foo, arg2: &'b Bar) -> &'b Baz +fn two_lifetimes<'a, 'b: 'a>(arg1: &'a Foo, arg2: &'b Bar) -> &'a Quux<'b> ``` you can write: ```rust -fn two_args(arg1: &Foo, arg2: &'b Bar) -> &'b Baz +fn two_args(arg1: &Foo, arg2: &Bar) -> &'arg2 Baz +fn two_lifetimes(arg1: &Foo, arg2: &Bar) -> &'arg1 Quux<'arg2> ``` -In particular, this RFC completely removes the need for listing lifetime +More generally, this RFC completely removes the need for listing lifetime parameters, instead binding them "in-place" (but with absolute clarity about *when* this binding is happening): ```rust fn named_lifetime(arg: &'inner Foo) -> &'inner Bar fn nested_lifetime(arg: &&'inner Foo) -> &'inner Bar +fn outer_lifetime(arg: &'outer &Foo) -> &'outer Bar ``` It also proposes linting 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. +- If an argument has a single elided lifetime, that lifetime is bound to the name of the argument. +- You can write `'_` to explicitly elide a lifetime. +- It is deprecated to: + - Bind lifetimes within the generics list `<>` for `impl`s and `fn`s. + - Implicitly elide lifetimes for non `&` types. +- The deprecations become errors at the next [epoch](https://github.com/rust-lang/rfcs/pull/2052). + +**This RFC does not introduce any breaking changes**, but does deprecate some +existing forms in favor of improved styles of expression. + # Motivation [motivation]: #motivation @@ -48,7 +64,7 @@ introduce a named lifetime parameter and refer to it throughout, which generally requires changing three parts of the signature: ```rust -fn two_args<'a, 'b>(arg1: &'a Foo, arg2: &'b Bar) -> &'b Baz +fn two_args<'a, 'b: 'a>(arg1: &'a Foo, arg2: &'b Bar) -> &'a Baz<'b> ``` In much idiomatic Rust code, these lifetime parameters are given meaningless @@ -207,9 +223,14 @@ which is a way of asking the compiler to determine their "intersection" returned `Item` borrow is valid for that period (which means it may incorporate data from both of the input borrows). -You can use any names you like when introducing lifetimes (which always start -with `'`), but are encouraged to make them meaningful, e.g. by using the same -name as the parameter. +In addition, when an argument has only one lifetime you could be referencing, +like `&Data`, you can refer to that lifetime by the argument's name: + +```rust +fn select(data: &Data, params: &Params) -> &'data Item; +``` + +Otherwise, you are not allowed to use an argument's name as a lifetime. ## `struct`s and lifetimes @@ -244,60 +265,55 @@ from. We can do something similar with structs: ```rust impl Vec { - fn iter(&self) -> VecIter<_, T> { ... } + fn iter(&self) -> VecIter<'_, T> { ... } } ``` -The `_` marker makes clear to the reader that *borrowing is happening*, which +The `'_` marker makes clear to the reader that *borrowing is happening*, which might not otherwise be clear. ## `impl` blocks and lifetimes When writing an `impl` block for a structure that takes a lifetime parameter, -you can give that parameter a name, but it must begin with an uppercase letter: +you can give that parameter a name: ```rust -impl VecIter<'Vec, T> { ... } +impl VecIter<'vec, T> { ... } ``` -The reason for this distinction is so that there's no potential for confusion -with lifetime names introduced by methods within the `impl` block. In other -words, it makes clear whether the method is *introducing* a new lifetime -(lowercase), or *referring* to the one in the `impl` header (uppercase): +This name can then be referred to in the body: ```rust -impl VecIter<'Vec, T> { - fn foo(&self) -> &'Vec T { ... } - fn bar(&self, arg: &'arg Bar) -> &'arg Bar { ... } +impl VecIter<'vec, T> { + fn foo(&self) -> &'vec T { ... } + fn bar(&self, arg: &Bar) -> &'arg Bar { ... } // these two are the same: fn baz(&self) -> &T { ... } - fn baz(&'self self) -> &'self T { ... } + fn baz(&self) -> &'self T { ... } } ``` -If the type's lifetime is not relevant, you can leave it off using `_`: +If the type's lifetime is not relevant, you can leave it off using `'_`: ```rust -impl VecIter<_, T> { ... } +impl VecIter<'_, T> { ... } ``` # Reference-level explanation [reference-level-explanation]: #reference-level-explanation +**Note: these changes are designed to *not* require a new epoch**. They +introduce several deprecations, which are essentially style lints. The next +epoch should turn these deprecations into errors. + ## Lifetimes in `impl` headers When writing an `impl` header, it is deprecated to bind a lifetime parameter -within the generics specification (e.g. `impl<'a>`). It is also deprecated to -use any lifetime variables that do not begin with a capital letter (which is -needed to clearly distinguish lifetimes that are bound in the `impl` header from -those bound in signatures). - -Instead the `impl` header can mention lifetimes without adding them as -generics. **These lifetimes *always* become parameters of the `impl`**--after -all, **no lifetime variables can possibly be in scope already**. In other words, -the lifetime parameters of an `impl` are taken to be the set of lifetime -variables it uses. +within the generics specification (e.g. `impl<'a>`). Instead the `impl` header +can mention lifetimes without adding them as generics. Any lifetimes that are +not already in scope (which, today, means any lifetime whatsoever) is treated as +being bound as a parameter of the `impl`. Thus, where today you would write: @@ -309,8 +325,8 @@ impl<'a, 'b> SomeTrait<'a> for SomeType<'a, 'b> { ... } tomorrow you would write: ```rust -impl Iterator for MyIter<'A> { ... } -impl SomeTrait<'A> for SomeType<'A, 'B> { ... } +impl Iterator for MyIter<'a> { ... } +impl SomeTrait<'a> for SomeType<'a, 'b> { ... } ``` ## Lifetimes in `fn` signatures @@ -320,13 +336,11 @@ within the generics specification (e.g. `fn foo<'a>(arg: &'a str)`). Instead: -- 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 - lifetime bound by the immediate containing `impl` header, and thus must occur - in that header. -- It is illegal for the return type to mention any lowercase lifetime variables - that do not occur in at least one argument. +- If a lifetime appears that is not already in scope, it is taken to be a new + binding, treated as a parameter to the function. +- If an argument has exactly one elided lifetime, you can refer to that lifetime + by the argument's name preceded by `'`. Otherwise, it is not permitted to use + that lifetime name (unless it is already in scope, which generates a warning). - As with today's elision rules, lifetimes that appear *only* within `Fn`-style bounds or trait object types are bound in higher-rank form (i.e., as if you'd written them using a `for<'a>`). @@ -335,7 +349,8 @@ Thus, where today you would write: ```rust fn elided(&self) -> &str; -fn two_args<'a, 'b>(arg1: &'a Foo, arg2: &'b Bar) -> &'b Baz; +fn two_args<'b>(arg1: &Foo, arg2: &'b Bar) -> &'b Baz +fn two_lifetimes<'a, 'b: 'a>(arg1: &'a Foo, arg2: &'b Bar) -> &'a Quux<'b> impl<'a> MyStruct<'a> { fn foo(&self) -> &'a str; @@ -349,14 +364,15 @@ tomorrow you would write: ```rust fn elided(&self) -> &str; -fn two_args(arg1: &Foo, arg2: &'b Bar) -> &'b Baz; +fn two_args(arg1: &Foo, arg2: &Bar) -> &'arg2 Baz +fn two_lifetimes(arg1: &Foo, arg2: &Bar) -> &'arg1 Quux<'arg2> impl MyStruct<'A> { fn foo(&self) -> &'A str; fn bar(&self, arg: &'b str) -> &'b str; } -fn take_fn(x: &'a u32, y: fn(&'a u32, &'b u32, &'b u32)); +fn take_fn(x: &u32, y: fn(&'x u32, &'b u32, &'b u32)); ``` ## The wildcard lifetime @@ -364,7 +380,7 @@ fn take_fn(x: &'a u32, y: fn(&'a u32, &'b u32, &'b u32)); When referring to a type (other than `&`/`&mut`) that requires lifetime arguments, it is deprecated to leave off those parameters. -Instead, you can write a `_` for the parameters, rather than giving a lifetime +Instead, you can write a `'_` for the parameters, rather than giving a lifetime name, which will have identical behavior to leaving them off today. Thus, where today you would write: @@ -377,30 +393,26 @@ fn iter(&self) -> Iter tomorrow you would write: ```rust -fn foo(&self) -> Ref<_, SomeType> -fn iter(&self) -> Iter<_, T> +fn foo(&self) -> Ref<'_, SomeType> +fn iter(&self) -> Iter<'_, T> ``` # Drawbacks [drawbacks]: #drawbacks -The deprecations here involve some amount of churn (largely in the form of -deleting lifetimes from `<>` blocks, but also sometimes changing their -case). Users exercise a lot of control over when they address that churn and can -do so incrementally. Moreover, we can and should consider addressing this with a -`rustfix`, which should be easy and highly reliable. +The deprecations here involve some amount of churn (in the form of deleting +lifetimes from `<>` blocks) if not ignored. Users exercise a lot of control over +when they address that churn and can do so incrementally, and we can likely +provide an automated tool for switching to the new style. The fact that lifetime parameters are not bound in an out-of-band way is somewhat unusual and might be confusing---but then, so are lifetime parameters! Putting the bindings out of band buys us very little, as argued in the next section. -Introducing a case distinction for lifetime names is a bit clunky, and of course -not all spoken languages *have* a case distinction (but for those, another -convention could be applied). - -Requiring a `_` rather than being able to leave off lifetimes altogether may be -a slight decrease in ergonomics in some cases. +Requiring a `'_` rather than being able to leave off lifetimes altogether may be +a slight decrease in ergonomics in some cases. In particular, `SomeType<'_>` is +pretty sigil-heavy. Cases where you could write `fn foo<'a, 'b: 'a>(...)` now need the `'b: 'a` to be given in a `where` clause, which might be slightly more verbose. These are @@ -425,57 +437,35 @@ parameters is buying us very little today: distinguishing between lifetimes bound by the `impl` header and those bounds by the `fn`. -**Thus, if we have some other way of distinguishing between `impl`-bound and -`fn`-bound lifetimes, binding lists for lifetimes are completely redundant**; we -can eliminate them without introducing any ambiguity. +While this might change if we ever allow modules to be parameterized by +lifetimes, it won't change in any essential way: the point is that there are +generally going to be *very* few in-scope lifetimes when writing a function +signature. We can likely use conventions or some other mechanism to help +distinguish between the `impl` header and `fn` bindings, if needed. -This RFC proposes to make the distinction through case; uppercase lifetimes are -already linted against today. However, we could instead distinguish it purely at +This RFC proposes to impose a strict distinction between lifetimes introduced in +`impl` headers and `fn` signatures. We could instead impose a distinction +through a convention, such as capitalization, and enforce the convention via a +lint. Alternatively, we could instead distinguish it purely at the use-site, for example by writing `outer('a)` or some such to refer to the -`impl` block bindings. In any case, having immediate visual clarity in `fn` -declarations as to whether a lifetime is coming from the `fn` or `impl` block is -a nice benefit. - -## Possible extension: "backreferences" - -This RFC was written with a particular extension in mind: allowing you to refer -to elided lifetimes through their parameter name, like so: - -```rust -fn scramble(&self, arg: &Foo) -> &'self Bar -``` - -Here, we are referring to `'self` in the return type without any references in -the argument---something that the RFC specifies will be an error. The idea is -that each parameter that involves a single, elided lifetime will be understood -to *bind* a lifetime using that parameter's name. - -For the sake of conservative, incremental progress, this RFC punts on the -extension, but tries to leave the door open to it. That will allow us to gather -more data before deciding on this additional step. (To be fully -forward-compatible, we probably need to make it an error to use the name of an -argument as a lifetime *unless* it is used in that argument as well.) - -Alternatively, we could consider including this extension up front, perhaps as a -feature gate, so that we can gain experience more quickly, but make -stabilization decisions separately. +`impl` block bindings. ## Alternatives -We could consider *only* allowing "backreferences", and otherwise keeping -binding as-is. However, that would forgo the benefits of eliminating out-of-band -binding, which would still be needed in some cases. +We could consider *only* allowing "backreferences" (i.e. references to argument +names), and otherwise keeping binding as-is. However, that would forgo the +benefits of eliminating out-of-band binding, which would still be needed in some +cases. Conversely, we could drop "backreferences", but that would reduce the win +for a lot of common cases. We could consider using this as an opportunity to eliminate `'` altogether, by tying these improvements to a new way of providing lifetimes, e.g. `&ref(x) T`. The [internals thread] on this topic covers a wide array of syntactic options -for leaving off a struct lifetime (which is `_` in this RFC), including: `'_`, -`&`, `ref`. The choice of `_` was informed by a few things: - -- It's short, evocative, and not too visually jarring. -- In signatures, it *always* means "lifetime elided", so there's a clear signal - re: borrowing. +for leaving off a struct lifetime (which is `'_` in this RFC), including: `_`, +`&`, `ref`. The choice of `'_` was driven by two factors: it's short, and it's +self-explanatory, given our use of wildcards elsewhere. On the other hand, the +syntax is pretty clunky. [internals thread]: (https://internals.rust-lang.org/t/lang-team-minutes-elision-2-0/5182) @@ -486,7 +476,4 @@ lifetimes from an `impl` header. # Unresolved questions [unresolved]: #unresolved-questions -- Should we include the "backreference" extension up front? It seems very likely - to be desired in this setup. - - Should we go further and eliminate the need for `for<'a>` notation as well? From 9a60b9031ba1c199948a8f0c39850ab41a89c03e Mon Sep 17 00:00:00 2001 From: Aaron Turon Date: Fri, 18 Aug 2017 14:33:32 -0700 Subject: [PATCH 3/8] Add unresolved question --- text/0000-argument-lifetimes.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/text/0000-argument-lifetimes.md b/text/0000-argument-lifetimes.md index 2b290bc97e3..bda9a7df836 100644 --- a/text/0000-argument-lifetimes.md +++ b/text/0000-argument-lifetimes.md @@ -6,8 +6,7 @@ # Summary [summary]: #summary -Improves the clarity, ergonomics, and learnability around explicit lifetimes, so -that instead of writing +Improves the clarity, ergonomics, and learnability around explicit lifetimes, so that instead of writing ```rust fn two_args<'b>(arg1: &Foo, arg2: &'b Bar) -> &'b Baz @@ -21,9 +20,7 @@ fn two_args(arg1: &Foo, arg2: &Bar) -> &'arg2 Baz fn two_lifetimes(arg1: &Foo, arg2: &Bar) -> &'arg1 Quux<'arg2> ``` -More generally, this RFC completely removes the need for listing lifetime -parameters, instead binding them "in-place" (but with absolute clarity about -*when* this binding is happening): +More generally, this RFC completely removes the need for listing lifetime parameters, instead binding them "in-place" (but with absolute clarity about *when* this binding is happening): ```rust fn named_lifetime(arg: &'inner Foo) -> &'inner Bar @@ -31,9 +28,7 @@ fn nested_lifetime(arg: &&'inner Foo) -> &'inner Bar fn outer_lifetime(arg: &'outer &Foo) -> &'outer Bar ``` -It also proposes linting 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). +It also proposes linting 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: @@ -45,8 +40,7 @@ The changes, in summary, are: - Implicitly elide lifetimes for non `&` types. - The deprecations become errors at the next [epoch](https://github.com/rust-lang/rfcs/pull/2052). -**This RFC does not introduce any breaking changes**, but does deprecate some -existing forms in favor of improved styles of expression. +**This RFC does not introduce any breaking changes**, but does deprecate some existing forms in favor of improved styles of expression. # Motivation [motivation]: #motivation @@ -477,3 +471,6 @@ lifetimes from an `impl` header. [unresolved]: #unresolved-questions - Should we go further and eliminate the need for `for<'a>` notation as well? + +- Should we introduce a style lint for imposing a convention distinguishing + between `impl` and `fn` lifetimes? From c20ea6db254feac3536de7ae8a13bfdee2b07b42 Mon Sep 17 00:00:00 2001 From: Aaron Turon Date: Sat, 26 Aug 2017 10:40:32 -0700 Subject: [PATCH 4/8] Drop backreferences --- text/0000-argument-lifetimes.md | 212 +++++++++++++++++--------------- 1 file changed, 112 insertions(+), 100 deletions(-) diff --git a/text/0000-argument-lifetimes.md b/text/0000-argument-lifetimes.md index bda9a7df836..b3b6fec6615 100644 --- a/text/0000-argument-lifetimes.md +++ b/text/0000-argument-lifetimes.md @@ -6,41 +6,42 @@ # Summary [summary]: #summary -Improves the clarity, ergonomics, and learnability around explicit lifetimes, so that instead of writing +Eliminate the need for separately binding lifetime parameters in `fn` +definitions and `impl` headers, so that instead of writing: ```rust fn two_args<'b>(arg1: &Foo, arg2: &'b Bar) -> &'b Baz -fn two_lifetimes<'a, 'b: 'a>(arg1: &'a Foo, arg2: &'b Bar) -> &'a Quux<'b> +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: ```rust -fn two_args(arg1: &Foo, arg2: &Bar) -> &'arg2 Baz -fn two_lifetimes(arg1: &Foo, arg2: &Bar) -> &'arg1 Quux<'arg2> -``` - -More generally, this RFC completely removes the need for listing lifetime parameters, instead binding them "in-place" (but with absolute clarity about *when* this binding is happening): +fn two_args(arg1: &Foo, arg2: &'b Bar) -> &'b Baz +fn two_lifetimes(arg1: &'a Foo, arg2: &'b Bar) -> &'a Quux<'b> -```rust -fn named_lifetime(arg: &'inner Foo) -> &'inner Bar fn nested_lifetime(arg: &&'inner Foo) -> &'inner Bar fn outer_lifetime(arg: &'outer &Foo) -> &'outer Bar ``` -It also proposes linting 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). +It also proposes linting 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. -- If an argument has a single elided lifetime, that lifetime is bound to the name of the argument. -- You can write `'_` to explicitly elide a lifetime. -- It is deprecated to: - - Bind lifetimes within the generics list `<>` for `impl`s and `fn`s. - - Implicitly elide lifetimes for non `&` types. -- The deprecations become errors at the next [epoch](https://github.com/rust-lang/rfcs/pull/2052). +- A style lint checks that lifetimes bound in `impl` headers are capitalized, to + avoid 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**, but does deprecate some existing forms in favor of improved styles of expression. +**This RFC does not introduce any breaking changes**, but does deprecate some +existing forms in favor of improved styles of expression. # Motivation [motivation]: #motivation @@ -61,15 +62,6 @@ requires changing three parts of the signature: fn two_args<'a, 'b: 'a>(arg1: &'a Foo, arg2: &'b Bar) -> &'a Baz<'b> ``` -In much idiomatic Rust code, these lifetime parameters are given meaningless -names like `'a`, because they're serving merely to tie pieces of the signature -together. This habit indicates a kind of design smell: we're forcing programmers -to conjure up and name a parameter whose identity doesn't matter to them. - -Moreover, when reading a signature involving lifetime parameters, you need to -scan the whole thing, keeping `'a` and `'b` in your head, to understand the -pattern of borrowing at play. - These concerns are just a papercut for advanced Rust users, but they also present a cliff in the learning curve, one affecting the most novel and difficult to learn part of Rust. In particular, when first explaining borrowing, @@ -88,7 +80,7 @@ the next section, I'll show how this RFC provides a gentler learning curve around lifetimes and disambiguation. Another point of confusion for newcomers and old hands alike is the fact that -you can leave lifetimes off when using types: +you can leave off lifetime parameters for types: ```rust struct Iter<'a> { ... } @@ -100,9 +92,10 @@ impl SomeType { As detailed in the [ergonomics initiative blog post], this bit of lifetime elision is considered a mistake: it makes it difficult to see at a glance that -borrowing is occurring, especially if you're unfamiliar with the types involved. -This RFC proposes some steps to rectify this situation without regressing -ergonomics significantly. +borrowing is occurring, especially if you're unfamiliar with the types +involved. (The `&` types, by contrast, are universally known to involve +borrowing.) This RFC proposes some steps to rectify this situation without +regressing ergonomics significantly. [ergonomics initiative blog post]: https://blog.rust-lang.org/2017/03/02/lang-ergonomics.html @@ -217,15 +210,6 @@ which is a way of asking the compiler to determine their "intersection" returned `Item` borrow is valid for that period (which means it may incorporate data from both of the input borrows). -In addition, when an argument has only one lifetime you could be referencing, -like `&Data`, you can refer to that lifetime by the argument's name: - -```rust -fn select(data: &Data, params: &Params) -> &'data Item; -``` - -Otherwise, you are not allowed to use an argument's name as a lifetime. - ## `struct`s and lifetimes Sometimes you need to build data types that contain borrowed data. Since those @@ -269,22 +253,19 @@ might not otherwise be clear. ## `impl` blocks and lifetimes When writing an `impl` block for a structure that takes a lifetime parameter, -you can give that parameter a name: +you can give that parameter a name, which by convention is capitalized (to +clearly distinguish it from lifetimes introduced at the `fn` level): ```rust -impl VecIter<'vec, T> { ... } +impl VecIter<'Vec, T> { ... } ``` This name can then be referred to in the body: ```rust -impl VecIter<'vec, T> { - fn foo(&self) -> &'vec T { ... } - fn bar(&self, arg: &Bar) -> &'arg Bar { ... } - - // these two are the same: - fn baz(&self) -> &T { ... } - fn baz(&self) -> &'self T { ... } +impl VecIter<'Vec, T> { + fn foo(&self) -> &'Vec T { ... } + fn bar(&self, arg: &'a Bar) -> &'a Bar { ... } } ``` @@ -297,17 +278,15 @@ impl VecIter<'_, T> { ... } # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -**Note: these changes are designed to *not* require a new epoch**. They -introduce several deprecations, which are essentially style lints. The next -epoch should turn these deprecations into errors. +**Note: these changes are designed to *not* require a new epoch**. They do +expand our naming style lint, however. ## Lifetimes in `impl` headers -When writing an `impl` header, it is deprecated to bind a lifetime parameter -within the generics specification (e.g. `impl<'a>`). Instead the `impl` header -can mention lifetimes without adding them as generics. Any lifetimes that are -not already in scope (which, today, means any lifetime whatsoever) is treated as -being bound as a parameter of the `impl`. +When writing an `impl` header, you can mention lifetimes without binding them in +the generics list. Any lifetimes that are not already in scope (which, today, +means any lifetime whatsoever) is treated as being bound as a parameter of the +`impl`. Thus, where today you would write: @@ -319,54 +298,51 @@ impl<'a, 'b> SomeTrait<'a> for SomeType<'a, 'b> { ... } tomorrow you would write: ```rust -impl Iterator for MyIter<'a> { ... } -impl SomeTrait<'a> for SomeType<'a, 'b> { ... } +impl Iterator for MyIter<'A> { ... } +impl SomeTrait<'A> for SomeType<'A, 'B> { ... } ``` -## Lifetimes in `fn` signatures - -When writing a `fn` declaration, it is deprecated to bind a lifetime parameter -within the generics specification (e.g. `fn foo<'a>(arg: &'a str)`). +This change goes hand-in-hand with a convention that lifetimes introduced in +`impl` headers (and perhaps someday, modules) are capitalized; this convention +will be enforced through the existing naming style lints. -Instead: +## Lifetimes in `fn` signatures -- If a lifetime appears that is not already in scope, it is taken to be a new - binding, treated as a parameter to the function. -- If an argument has exactly one elided lifetime, you can refer to that lifetime - by the argument's name preceded by `'`. Otherwise, it is not permitted to use - that lifetime name (unless it is already in scope, which generates a warning). -- As with today's elision rules, lifetimes that appear *only* within `Fn`-style - bounds or trait object types are bound in higher-rank form (i.e., as if you'd - written them using a `for<'a>`). +When writing a `fn` declaration, if a lifetime appears that is not already in +scope, it is taken to be a new binding, i.e. treated as a parameter to the +function. **This rule applies regardless of where the lifetime +appears**. However, elision for higher-ranked types continues to work as today. Thus, where today you would write: ```rust -fn elided(&self) -> &str; +fn elided(&self) -> &str fn two_args<'b>(arg1: &Foo, arg2: &'b Bar) -> &'b Baz fn two_lifetimes<'a, 'b: 'a>(arg1: &'a Foo, arg2: &'b Bar) -> &'a Quux<'b> impl<'a> MyStruct<'a> { - fn foo(&self) -> &'a str; - fn bar<'b>(&self, arg: &'b str) -> &'b str; + fn foo(&self) -> &'a str + fn bar<'b>(&self, arg: &'b str) -> &'b str } +fn take_fn_simple(f: fn(&Foo) -> &Bar) fn take_fn<'a>(x: &'a u32, y: for<'b> fn(&'a u32, &'b u32, &'b u32)) ``` tomorrow you would write: ```rust -fn elided(&self) -> &str; +fn elided(&self) -> &str fn two_args(arg1: &Foo, arg2: &Bar) -> &'arg2 Baz fn two_lifetimes(arg1: &Foo, arg2: &Bar) -> &'arg1 Quux<'arg2> impl MyStruct<'A> { - fn foo(&self) -> &'A str; - fn bar(&self, arg: &'b str) -> &'b str; + fn foo(&self) -> &'A str + fn bar(&self, arg: &'b str) -> &'b str } -fn take_fn(x: &u32, y: fn(&'x u32, &'b u32, &'b u32)); +fn take_fn_simple(f: fn(&Foo) -> &Bar) +fn take_fn(x: &'a u32, y: for<'b> fn(&'a u32, &'b u32, &'b u32)) ``` ## The wildcard lifetime @@ -391,13 +367,26 @@ fn foo(&self) -> Ref<'_, SomeType> fn iter(&self) -> Iter<'_, T> ``` +## Additional lints + +Beyond the change to the style lint for capitalizing `impl` header lifetimes, +two more lints are provided: + +- One deny-by-default lint against `fn` definitions in which a lifetime occurs + exactly once. Such lifetimes can always be replaced by `'_` (or for `&`, + elided altogether), and giving an explicit name is confusing at best, and + indicates a typo at worst. + +- An expansion of Clippy's lints so that they warn when a signature contains + other unnecessary elements, e.g. when it could be using elision or could leave + off lifetimes from its generics list. + # Drawbacks [drawbacks]: #drawbacks -The deprecations here involve some amount of churn (in the form of deleting -lifetimes from `<>` blocks) if not ignored. Users exercise a lot of control over -when they address that churn and can do so incrementally, and we can likely -provide an automated tool for switching to the new style. +The style lint for `impl` headers could introduce some amount of churn. This +could be mitigated by only applying that lint for lifetimes not bound in the +generics list. The fact that lifetime parameters are not bound in an out-of-band way is somewhat unusual and might be confusing---but then, so are lifetime parameters! @@ -412,7 +401,7 @@ Cases where you could write `fn foo<'a, 'b: 'a>(...)` now need the `'b: 'a` to be given in a `where` clause, which might be slightly more verbose. These are relatively rare, though, due to our type well-formedness rule. -Otherwise, it's a bit hard to see drawbacks here: nothings is made more explicit +Otherwise, it's a bit hard to see drawbacks here: nothings is made less explicit or harder to determine, since the binding structure continues to be completely unambiguous; ergonomics and, arguably, learnability both improve. And signatures become less noisy and easier to read. @@ -434,23 +423,48 @@ parameters is buying us very little today: While this might change if we ever allow modules to be parameterized by lifetimes, it won't change in any essential way: the point is that there are generally going to be *very* few in-scope lifetimes when writing a function -signature. We can likely use conventions or some other mechanism to help -distinguish between the `impl` header and `fn` bindings, if needed. +signature. So the premise is that we can use naming conventions to distinguish +between the `impl` header (or eventual module headers) and `fn` bindings. -This RFC proposes to impose a strict distinction between lifetimes introduced in -`impl` headers and `fn` signatures. We could instead impose a distinction -through a convention, such as capitalization, and enforce the convention via a -lint. Alternatively, we could instead distinguish it purely at -the use-site, for example by writing `outer('a)` or some such to refer to the -`impl` block bindings. +Alternatively, we could instead distinguish these cases at the use-site, for +example by writing `outer('a)` or some such to refer to the `impl` block +bindings. -## Alternatives +## Possible extension or alternative: "backreferences" + +A different approach would be refering to elided lifetimes through their +parameter name, like so: + +```rust +fn scramble(&self, arg: &Foo) -> &'self Bar +``` + +The idea is that each parameter that involves a single, elided lifetime will be +understood to *bind* a lifetime using that parameter's name. + +Earlier iterations of this RFC combined these "backreferences" with the rest of +the proposal, but this was deemed too confusing and error-prone, and in +particular harmed readability by requiring you to scan both lifetime mentions +*and* parameter names. We could consider *only* allowing "backreferences" (i.e. references to argument -names), and otherwise keeping binding as-is. However, that would forgo the -benefits of eliminating out-of-band binding, which would still be needed in some -cases. Conversely, we could drop "backreferences", but that would reduce the win -for a lot of common cases. +names), and otherwise keeping binding as-is. However, this has a few downsides: + +- It doesn't help with `impl` headers +- It doesn't entirely eliminate the need for lifetimes in generics lists for + `fn` definitions, meaning that there's still *another* step of learning to + reach fully expressive lifetimes. +- As @rpjohnst [argued](https://github.com/rust-lang/rfcs/pull/2115#issuecomment-324147717), + backreferences can end up reinforcing an importantly-wrong mental model, namely + that you're borrowing from an argument, rather than from its (already-borrowed) + contents. By contrast, requiring you to write the lifetime reinforces the opposite + idea: that borrowing has already occurred, and that what you're tying together is + that existing lifetime. +- On a similar note, using backreferences to tie multiple arguments together is + often nonsensical, since there's no sense in which one argument is the "primary + definer" of the lifetime. + +## Alternatives We could consider using this as an opportunity to eliminate `'` altogether, by tying these improvements to a new way of providing lifetimes, e.g. `&ref(x) T`. @@ -470,7 +484,5 @@ lifetimes from an `impl` header. # Unresolved questions [unresolved]: #unresolved-questions -- Should we go further and eliminate the need for `for<'a>` notation as well? - -- Should we introduce a style lint for imposing a convention distinguishing - between `impl` and `fn` lifetimes? +- Should we introduce higher-ranked bounds automatically when using named + lifetimes in e.g. an embedded `fn` type? From 0f4b2162b0813da0c6fa5d1bf546e72c70372100 Mon Sep 17 00:00:00 2001 From: Aaron Turon Date: Sat, 26 Aug 2017 10:41:44 -0700 Subject: [PATCH 5/8] Clarify summary --- text/0000-argument-lifetimes.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/text/0000-argument-lifetimes.md b/text/0000-argument-lifetimes.md index b3b6fec6615..8653215436d 100644 --- a/text/0000-argument-lifetimes.md +++ b/text/0000-argument-lifetimes.md @@ -40,8 +40,7 @@ The changes, in summary, are: - 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**, but does deprecate some -existing forms in favor of improved styles of expression. +**This RFC does not introduce any breaking changes**. # Motivation [motivation]: #motivation From efa1f3dd86bbfc9061d36b0a4faa802e1feb0c9b Mon Sep 17 00:00:00 2001 From: Aaron Turon Date: Thu, 31 Aug 2017 08:43:00 -0700 Subject: [PATCH 6/8] Future-proofing updates --- text/0000-argument-lifetimes.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/text/0000-argument-lifetimes.md b/text/0000-argument-lifetimes.md index 8653215436d..785ecc42745 100644 --- a/text/0000-argument-lifetimes.md +++ b/text/0000-argument-lifetimes.md @@ -309,8 +309,7 @@ will be enforced through the existing naming style lints. When writing a `fn` declaration, if a lifetime appears that is not already in scope, it is taken to be a new binding, i.e. treated as a parameter to the -function. **This rule applies regardless of where the lifetime -appears**. However, elision for higher-ranked types continues to work as today. +function. Thus, where today you would write: @@ -344,6 +343,17 @@ fn take_fn_simple(f: fn(&Foo) -> &Bar) fn take_fn(x: &'a u32, y: for<'b> fn(&'a u32, &'b u32, &'b u32)) ``` +For higher-ranked types (including cases like `Fn` syntax), elision works as it +does today. However, **it is an error to mention a lifetime in a higher-ranked +type that hasn't been explicitly bound** (either at the outer `fn` definition, +or within an explicit `for<>`). These cases are extremely rare, and making them +an error keeps our options open for providing an interpretation later on. + +Similarly, if a `fn` definition is nested inside another `fn` definition, it is +an error to mention lifetimes from that outer definition (without binding them +explicitly). This is again intended for future-proofing and clarity, and is an +edge case. + ## The wildcard lifetime When referring to a type (other than `&`/`&mut`) that requires lifetime @@ -483,5 +493,4 @@ lifetimes from an `impl` header. # Unresolved questions [unresolved]: #unresolved-questions -- Should we introduce higher-ranked bounds automatically when using named - lifetimes in e.g. an embedded `fn` type? +- How to treat examples like `fn f() -> &'a str { "static string" }`. From e8107ffcce5de6309b68578d327cc89b1b6902b1 Mon Sep 17 00:00:00 2001 From: Aaron Turon Date: Tue, 5 Sep 2017 19:00:58 -0700 Subject: [PATCH 7/8] Another round of revisions --- text/0000-argument-lifetimes.md | 53 ++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/text/0000-argument-lifetimes.md b/text/0000-argument-lifetimes.md index 785ecc42745..11c42b9336e 100644 --- a/text/0000-argument-lifetimes.md +++ b/text/0000-argument-lifetimes.md @@ -27,16 +27,16 @@ fn nested_lifetime(arg: &&'inner Foo) -> &'inner Bar fn outer_lifetime(arg: &'outer &Foo) -> &'outer Bar ``` -It also proposes linting 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). +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 capitalized, to - avoid confusion with lifetimes bound within functions. (There are some - additional, less important lints proposed as well.) +- 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 @@ -252,18 +252,18 @@ might not otherwise be clear. ## `impl` blocks and lifetimes When writing an `impl` block for a structure that takes a lifetime parameter, -you can give that parameter a name, which by convention is capitalized (to -clearly distinguish it from lifetimes introduced at the `fn` level): +you can give that parameter a name, which you should strive to make +*meaningful*: ```rust -impl VecIter<'Vec, T> { ... } +impl VecIter<'vec, T> { ... } ``` This name can then be referred to in the body: ```rust -impl VecIter<'Vec, T> { - fn foo(&self) -> &'Vec T { ... } +impl VecIter<'vec, T> { + fn foo(&self) -> &'vec T { ... } fn bar(&self, arg: &'a Bar) -> &'a Bar { ... } } ``` @@ -297,13 +297,16 @@ impl<'a, 'b> SomeTrait<'a> for SomeType<'a, 'b> { ... } tomorrow you would write: ```rust -impl Iterator for MyIter<'A> { ... } -impl SomeTrait<'A> for SomeType<'A, 'B> { ... } +impl Iterator for MyIter<'iter> { ... } +impl SomeTrait<'tcx, 'gcx> for SomeType<'tcx, 'gcx> { ... } ``` +If any lifetime names are explicitly bound, they all must be. + This change goes hand-in-hand with a convention that lifetimes introduced in -`impl` headers (and perhaps someday, modules) are capitalized; this convention -will be enforced through the existing naming style lints. +`impl` headers (and perhaps someday, modules) should be multiple characters, +i.e. "meaningful" names, to reduce the chance of collision with typical `'a` +usage in functions. ## Lifetimes in `fn` signatures @@ -343,6 +346,8 @@ fn take_fn_simple(f: fn(&Foo) -> &Bar) fn take_fn(x: &'a u32, y: for<'b> fn(&'a u32, &'b u32, &'b u32)) ``` +If any lifetime names are explicitly bound, they all must be. + For higher-ranked types (including cases like `Fn` syntax), elision works as it does today. However, **it is an error to mention a lifetime in a higher-ranked type that hasn't been explicitly bound** (either at the outer `fn` definition, @@ -378,12 +383,12 @@ fn iter(&self) -> Iter<'_, T> ## Additional lints -Beyond the change to the style lint for capitalizing `impl` header lifetimes, -two more lints are provided: +Beyond the change to the style lint for `impl` header lifetimes, two more lints +are provided: -- One deny-by-default lint against `fn` definitions in which a lifetime occurs - exactly once. Such lifetimes can always be replaced by `'_` (or for `&`, - elided altogether), and giving an explicit name is confusing at best, and +- One deny-by-default lint against `fn` definitions in which an unbound lifetime + occurs exactly once. Such lifetimes can always be replaced by `'_` (or for + `&`, elided altogether), and giving an explicit name is confusing at best, and indicates a typo at worst. - An expansion of Clippy's lints so that they warn when a signature contains @@ -402,6 +407,14 @@ somewhat unusual and might be confusing---but then, so are lifetime parameters! Putting the bindings out of band buys us very little, as argued in the next section. +It's possible that the inconsistency with type parameters, which must always be +bound explicitly, will be confusing. In particular, lifetime parameters for +`struct` definitions appear side-by-side with parameter lists, but elsewhere are +bound differently. However, users are virtually certain to encounter type +generics prior to explicit lifetime generics, and if they try to follow the same +style -- by binding lifetime parameters explicitly -- that will work just fine +(but may be linted in Clippy as unnecessary). + Requiring a `'_` rather than being able to leave off lifetimes altogether may be a slight decrease in ergonomics in some cases. In particular, `SomeType<'_>` is pretty sigil-heavy. From 93259c2fb9a66d805d9f5c9c049df82aad2b5553 Mon Sep 17 00:00:00 2001 From: Aaron Turon Date: Tue, 12 Sep 2017 11:52:15 -0700 Subject: [PATCH 8/8] RFC 2115: In-band lifetime bindings --- ...{0000-argument-lifetimes.md => 2115-argument-lifetimes.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename text/{0000-argument-lifetimes.md => 2115-argument-lifetimes.md} (99%) diff --git a/text/0000-argument-lifetimes.md b/text/2115-argument-lifetimes.md similarity index 99% rename from text/0000-argument-lifetimes.md rename to text/2115-argument-lifetimes.md index 11c42b9336e..739aa41485b 100644 --- a/text/0000-argument-lifetimes.md +++ b/text/2115-argument-lifetimes.md @@ -1,7 +1,7 @@ - Feature Name: argument_lifetimes - Start Date: 2017-08-17 -- RFC PR: (leave this empty) -- Rust Issue: (leave this empty) +- RFC PR: https://github.com/rust-lang/rfcs/pull/2115 +- Rust Issue: https://github.com/rust-lang/rust/issues/44524 # Summary [summary]: #summary