Skip to content

Commit c20ea6d

Browse files
committed
Drop backreferences
1 parent 9a60b90 commit c20ea6d

File tree

1 file changed

+112
-100
lines changed

1 file changed

+112
-100
lines changed

text/0000-argument-lifetimes.md

Lines changed: 112 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,42 @@
66
# Summary
77
[summary]: #summary
88

9-
Improves the clarity, ergonomics, and learnability around explicit lifetimes, so that instead of writing
9+
Eliminate the need for separately binding lifetime parameters in `fn`
10+
definitions and `impl` headers, so that instead of writing:
1011

1112
```rust
1213
fn two_args<'b>(arg1: &Foo, arg2: &'b Bar) -> &'b Baz
13-
fn two_lifetimes<'a, 'b: 'a>(arg1: &'a Foo, arg2: &'b Bar) -> &'a Quux<'b>
14+
fn two_lifetimes<'a, 'b>(arg1: &'a Foo, arg2: &'b Bar) -> &'a Quux<'b>
15+
16+
fn nested_lifetime<'inner>(arg: &&'inner Foo) -> &'inner Bar
17+
fn outer_lifetime<'outer>(arg: &'outer &Foo) -> &'outer Bar
1418
```
1519

1620
you can write:
1721

1822
```rust
19-
fn two_args(arg1: &Foo, arg2: &Bar) -> &'arg2 Baz
20-
fn two_lifetimes(arg1: &Foo, arg2: &Bar) -> &'arg1 Quux<'arg2>
21-
```
22-
23-
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):
23+
fn two_args(arg1: &Foo, arg2: &'b Bar) -> &'b Baz
24+
fn two_lifetimes(arg1: &'a Foo, arg2: &'b Bar) -> &'a Quux<'b>
2425

25-
```rust
26-
fn named_lifetime(arg: &'inner Foo) -> &'inner Bar
2726
fn nested_lifetime(arg: &&'inner Foo) -> &'inner Bar
2827
fn outer_lifetime(arg: &'outer &Foo) -> &'outer Bar
2928
```
3029

31-
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).
30+
It also proposes linting against leaving off lifetime parameters in structs
31+
(like `Ref` or `Iter`), instead nudging people to use explicit lifetimes in this
32+
case (but leveraging the other improvements to make it ergonomic to do so).
3233

3334
The changes, in summary, are:
3435

3536
- A signature is taken to bind any lifetimes it mentions that are not already bound.
36-
- If an argument has a single elided lifetime, that lifetime is bound to the name of the argument.
37-
- You can write `'_` to explicitly elide a lifetime.
38-
- It is deprecated to:
39-
- Bind lifetimes within the generics list `<>` for `impl`s and `fn`s.
40-
- Implicitly elide lifetimes for non `&` types.
41-
- The deprecations become errors at the next [epoch](https://github.com/rust-lang/rfcs/pull/2052).
37+
- A style lint checks that lifetimes bound in `impl` headers are capitalized, to
38+
avoid confusion with lifetimes bound within functions. (There are some
39+
additional, less important lints proposed as well.)
40+
- You can write `'_` to explicitly elide a lifetime, and it is deprecated to
41+
entirely leave off lifetime arguments for non-`&` types
4242

43-
**This RFC does not introduce any breaking changes**, but does deprecate some existing forms in favor of improved styles of expression.
43+
**This RFC does not introduce any breaking changes**, but does deprecate some
44+
existing forms in favor of improved styles of expression.
4445

4546
# Motivation
4647
[motivation]: #motivation
@@ -61,15 +62,6 @@ requires changing three parts of the signature:
6162
fn two_args<'a, 'b: 'a>(arg1: &'a Foo, arg2: &'b Bar) -> &'a Baz<'b>
6263
```
6364

64-
In much idiomatic Rust code, these lifetime parameters are given meaningless
65-
names like `'a`, because they're serving merely to tie pieces of the signature
66-
together. This habit indicates a kind of design smell: we're forcing programmers
67-
to conjure up and name a parameter whose identity doesn't matter to them.
68-
69-
Moreover, when reading a signature involving lifetime parameters, you need to
70-
scan the whole thing, keeping `'a` and `'b` in your head, to understand the
71-
pattern of borrowing at play.
72-
7365
These concerns are just a papercut for advanced Rust users, but they also
7466
present a cliff in the learning curve, one affecting the most novel and
7567
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
8880
around lifetimes and disambiguation.
8981

9082
Another point of confusion for newcomers and old hands alike is the fact that
91-
you can leave lifetimes off when using types:
83+
you can leave off lifetime parameters for types:
9284

9385
```rust
9486
struct Iter<'a> { ... }
@@ -100,9 +92,10 @@ impl SomeType {
10092

10193
As detailed in the [ergonomics initiative blog post], this bit of lifetime
10294
elision is considered a mistake: it makes it difficult to see at a glance that
103-
borrowing is occurring, especially if you're unfamiliar with the types involved.
104-
This RFC proposes some steps to rectify this situation without regressing
105-
ergonomics significantly.
95+
borrowing is occurring, especially if you're unfamiliar with the types
96+
involved. (The `&` types, by contrast, are universally known to involve
97+
borrowing.) This RFC proposes some steps to rectify this situation without
98+
regressing ergonomics significantly.
10699

107100
[ergonomics initiative blog post]: https://blog.rust-lang.org/2017/03/02/lang-ergonomics.html
108101

@@ -217,15 +210,6 @@ which is a way of asking the compiler to determine their "intersection"
217210
returned `Item` borrow is valid for that period (which means it may incorporate
218211
data from both of the input borrows).
219212

220-
In addition, when an argument has only one lifetime you could be referencing,
221-
like `&Data`, you can refer to that lifetime by the argument's name:
222-
223-
```rust
224-
fn select(data: &Data, params: &Params) -> &'data Item;
225-
```
226-
227-
Otherwise, you are not allowed to use an argument's name as a lifetime.
228-
229213
## `struct`s and lifetimes
230214

231215
Sometimes you need to build data types that contain borrowed data. Since those
@@ -269,22 +253,19 @@ might not otherwise be clear.
269253
## `impl` blocks and lifetimes
270254

271255
When writing an `impl` block for a structure that takes a lifetime parameter,
272-
you can give that parameter a name:
256+
you can give that parameter a name, which by convention is capitalized (to
257+
clearly distinguish it from lifetimes introduced at the `fn` level):
273258

274259
```rust
275-
impl<T> VecIter<'vec, T> { ... }
260+
impl<T> VecIter<'Vec, T> { ... }
276261
```
277262

278263
This name can then be referred to in the body:
279264

280265
```rust
281-
impl<T> VecIter<'vec, T> {
282-
fn foo(&self) -> &'vec T { ... }
283-
fn bar(&self, arg: &Bar) -> &'arg Bar { ... }
284-
285-
// these two are the same:
286-
fn baz(&self) -> &T { ... }
287-
fn baz(&self) -> &'self T { ... }
266+
impl<T> VecIter<'Vec, T> {
267+
fn foo(&self) -> &'Vec T { ... }
268+
fn bar(&self, arg: &'a Bar) -> &'a Bar { ... }
288269
}
289270
```
290271

@@ -297,17 +278,15 @@ impl<T> VecIter<'_, T> { ... }
297278
# Reference-level explanation
298279
[reference-level-explanation]: #reference-level-explanation
299280

300-
**Note: these changes are designed to *not* require a new epoch**. They
301-
introduce several deprecations, which are essentially style lints. The next
302-
epoch should turn these deprecations into errors.
281+
**Note: these changes are designed to *not* require a new epoch**. They do
282+
expand our naming style lint, however.
303283

304284
## Lifetimes in `impl` headers
305285

306-
When writing an `impl` header, it is deprecated to bind a lifetime parameter
307-
within the generics specification (e.g. `impl<'a>`). Instead the `impl` header
308-
can mention lifetimes without adding them as generics. Any lifetimes that are
309-
not already in scope (which, today, means any lifetime whatsoever) is treated as
310-
being bound as a parameter of the `impl`.
286+
When writing an `impl` header, you can mention lifetimes without binding them in
287+
the generics list. Any lifetimes that are not already in scope (which, today,
288+
means any lifetime whatsoever) is treated as being bound as a parameter of the
289+
`impl`.
311290

312291
Thus, where today you would write:
313292

@@ -319,54 +298,51 @@ impl<'a, 'b> SomeTrait<'a> for SomeType<'a, 'b> { ... }
319298
tomorrow you would write:
320299

321300
```rust
322-
impl Iterator for MyIter<'a> { ... }
323-
impl SomeTrait<'a> for SomeType<'a, 'b> { ... }
301+
impl Iterator for MyIter<'A> { ... }
302+
impl SomeTrait<'A> for SomeType<'A, 'B> { ... }
324303
```
325304

326-
## Lifetimes in `fn` signatures
327-
328-
When writing a `fn` declaration, it is deprecated to bind a lifetime parameter
329-
within the generics specification (e.g. `fn foo<'a>(arg: &'a str)`).
305+
This change goes hand-in-hand with a convention that lifetimes introduced in
306+
`impl` headers (and perhaps someday, modules) are capitalized; this convention
307+
will be enforced through the existing naming style lints.
330308

331-
Instead:
309+
## Lifetimes in `fn` signatures
332310

333-
- If a lifetime appears that is not already in scope, it is taken to be a new
334-
binding, treated as a parameter to the function.
335-
- If an argument has exactly one elided lifetime, you can refer to that lifetime
336-
by the argument's name preceded by `'`. Otherwise, it is not permitted to use
337-
that lifetime name (unless it is already in scope, which generates a warning).
338-
- As with today's elision rules, lifetimes that appear *only* within `Fn`-style
339-
bounds or trait object types are bound in higher-rank form (i.e., as if you'd
340-
written them using a `for<'a>`).
311+
When writing a `fn` declaration, if a lifetime appears that is not already in
312+
scope, it is taken to be a new binding, i.e. treated as a parameter to the
313+
function. **This rule applies regardless of where the lifetime
314+
appears**. However, elision for higher-ranked types continues to work as today.
341315

342316
Thus, where today you would write:
343317

344318
```rust
345-
fn elided(&self) -> &str;
319+
fn elided(&self) -> &str
346320
fn two_args<'b>(arg1: &Foo, arg2: &'b Bar) -> &'b Baz
347321
fn two_lifetimes<'a, 'b: 'a>(arg1: &'a Foo, arg2: &'b Bar) -> &'a Quux<'b>
348322

349323
impl<'a> MyStruct<'a> {
350-
fn foo(&self) -> &'a str;
351-
fn bar<'b>(&self, arg: &'b str) -> &'b str;
324+
fn foo(&self) -> &'a str
325+
fn bar<'b>(&self, arg: &'b str) -> &'b str
352326
}
353327

328+
fn take_fn_simple(f: fn(&Foo) -> &Bar)
354329
fn take_fn<'a>(x: &'a u32, y: for<'b> fn(&'a u32, &'b u32, &'b u32))
355330
```
356331

357332
tomorrow you would write:
358333

359334
```rust
360-
fn elided(&self) -> &str;
335+
fn elided(&self) -> &str
361336
fn two_args(arg1: &Foo, arg2: &Bar) -> &'arg2 Baz
362337
fn two_lifetimes(arg1: &Foo, arg2: &Bar) -> &'arg1 Quux<'arg2>
363338

364339
impl MyStruct<'A> {
365-
fn foo(&self) -> &'A str;
366-
fn bar(&self, arg: &'b str) -> &'b str;
340+
fn foo(&self) -> &'A str
341+
fn bar(&self, arg: &'b str) -> &'b str
367342
}
368343

369-
fn take_fn(x: &u32, y: fn(&'x u32, &'b u32, &'b u32));
344+
fn take_fn_simple(f: fn(&Foo) -> &Bar)
345+
fn take_fn(x: &'a u32, y: for<'b> fn(&'a u32, &'b u32, &'b u32))
370346
```
371347

372348
## The wildcard lifetime
@@ -391,13 +367,26 @@ fn foo(&self) -> Ref<'_, SomeType>
391367
fn iter(&self) -> Iter<'_, T>
392368
```
393369

370+
## Additional lints
371+
372+
Beyond the change to the style lint for capitalizing `impl` header lifetimes,
373+
two more lints are provided:
374+
375+
- One deny-by-default lint against `fn` definitions in which a lifetime occurs
376+
exactly once. Such lifetimes can always be replaced by `'_` (or for `&`,
377+
elided altogether), and giving an explicit name is confusing at best, and
378+
indicates a typo at worst.
379+
380+
- An expansion of Clippy's lints so that they warn when a signature contains
381+
other unnecessary elements, e.g. when it could be using elision or could leave
382+
off lifetimes from its generics list.
383+
394384
# Drawbacks
395385
[drawbacks]: #drawbacks
396386

397-
The deprecations here involve some amount of churn (in the form of deleting
398-
lifetimes from `<>` blocks) if not ignored. Users exercise a lot of control over
399-
when they address that churn and can do so incrementally, and we can likely
400-
provide an automated tool for switching to the new style.
387+
The style lint for `impl` headers could introduce some amount of churn. This
388+
could be mitigated by only applying that lint for lifetimes not bound in the
389+
generics list.
401390

402391
The fact that lifetime parameters are not bound in an out-of-band way is
403392
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
412401
be given in a `where` clause, which might be slightly more verbose. These are
413402
relatively rare, though, due to our type well-formedness rule.
414403

415-
Otherwise, it's a bit hard to see drawbacks here: nothings is made more explicit
404+
Otherwise, it's a bit hard to see drawbacks here: nothings is made less explicit
416405
or harder to determine, since the binding structure continues to be completely
417406
unambiguous; ergonomics and, arguably, learnability both improve. And
418407
signatures become less noisy and easier to read.
@@ -434,23 +423,48 @@ parameters is buying us very little today:
434423
While this might change if we ever allow modules to be parameterized by
435424
lifetimes, it won't change in any essential way: the point is that there are
436425
generally going to be *very* few in-scope lifetimes when writing a function
437-
signature. We can likely use conventions or some other mechanism to help
438-
distinguish between the `impl` header and `fn` bindings, if needed.
426+
signature. So the premise is that we can use naming conventions to distinguish
427+
between the `impl` header (or eventual module headers) and `fn` bindings.
439428

440-
This RFC proposes to impose a strict distinction between lifetimes introduced in
441-
`impl` headers and `fn` signatures. We could instead impose a distinction
442-
through a convention, such as capitalization, and enforce the convention via a
443-
lint. Alternatively, we could instead distinguish it purely at
444-
the use-site, for example by writing `outer('a)` or some such to refer to the
445-
`impl` block bindings.
429+
Alternatively, we could instead distinguish these cases at the use-site, for
430+
example by writing `outer('a)` or some such to refer to the `impl` block
431+
bindings.
446432

447-
## Alternatives
433+
## Possible extension or alternative: "backreferences"
434+
435+
A different approach would be refering to elided lifetimes through their
436+
parameter name, like so:
437+
438+
```rust
439+
fn scramble(&self, arg: &Foo) -> &'self Bar
440+
```
441+
442+
The idea is that each parameter that involves a single, elided lifetime will be
443+
understood to *bind* a lifetime using that parameter's name.
444+
445+
Earlier iterations of this RFC combined these "backreferences" with the rest of
446+
the proposal, but this was deemed too confusing and error-prone, and in
447+
particular harmed readability by requiring you to scan both lifetime mentions
448+
*and* parameter names.
448449

449450
We could consider *only* allowing "backreferences" (i.e. references to argument
450-
names), and otherwise keeping binding as-is. However, that would forgo the
451-
benefits of eliminating out-of-band binding, which would still be needed in some
452-
cases. Conversely, we could drop "backreferences", but that would reduce the win
453-
for a lot of common cases.
451+
names), and otherwise keeping binding as-is. However, this has a few downsides:
452+
453+
- It doesn't help with `impl` headers
454+
- It doesn't entirely eliminate the need for lifetimes in generics lists for
455+
`fn` definitions, meaning that there's still *another* step of learning to
456+
reach fully expressive lifetimes.
457+
- As @rpjohnst [argued](https://github.com/rust-lang/rfcs/pull/2115#issuecomment-324147717),
458+
backreferences can end up reinforcing an importantly-wrong mental model, namely
459+
that you're borrowing from an argument, rather than from its (already-borrowed)
460+
contents. By contrast, requiring you to write the lifetime reinforces the opposite
461+
idea: that borrowing has already occurred, and that what you're tying together is
462+
that existing lifetime.
463+
- On a similar note, using backreferences to tie multiple arguments together is
464+
often nonsensical, since there's no sense in which one argument is the "primary
465+
definer" of the lifetime.
466+
467+
## Alternatives
454468

455469
We could consider using this as an opportunity to eliminate `'` altogether, by
456470
tying these improvements to a new way of providing lifetimes, e.g. `&ref(x) T`.
@@ -470,7 +484,5 @@ lifetimes from an `impl` header.
470484
# Unresolved questions
471485
[unresolved]: #unresolved-questions
472486

473-
- Should we go further and eliminate the need for `for<'a>` notation as well?
474-
475-
- Should we introduce a style lint for imposing a convention distinguishing
476-
between `impl` and `fn` lifetimes?
487+
- Should we introduce higher-ranked bounds automatically when using named
488+
lifetimes in e.g. an embedded `fn` type?

0 commit comments

Comments
 (0)