Skip to content

Commit e09e4a8

Browse files
committed
Reorganize scoping rules sections
1 parent 2274ce3 commit e09e4a8

File tree

1 file changed

+103
-107
lines changed

1 file changed

+103
-107
lines changed

text/0000-return-position-impl-trait-in-traits.md

Lines changed: 103 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -179,20 +179,25 @@ impl NewIntoIterator for Vec<u32> {
179179
}
180180
```
181181

182-
## Generic parameter capture and GATs
182+
## Scoping rules for generic parameters
183183

184-
Given a trait method with a return type like `-> impl A + ... + Z` and an implementation of that trait, the hidden type for that implementation is allowed to reference:
184+
We say a generic parameter is "in scope" for an `impl Trait` type if the actual revealed type is allowed to name that parameter. The scoping rules for return position `impl Trait` in traits are the same as [those for return position `impl Trait` generally][scoping]: All type and const parameters are considered in-scope, while lifetime parameters are only considered in-scope if they are mentioned in the `impl Trait` type directly.
185+
186+
Formally, given a trait method with a return type like `-> impl A + ... + Z` and an implementation of that trait, the hidden type for that implementation is allowed to reference:
185187

186188
* Concrete types, constant expressions, and `'static`
187-
* `Self`
188-
* Generics on the impl
189-
* Certain generics on the method
190-
* Explicit type parameters
191-
* Argument-position `impl Trait` types
192-
* Explicit const parameters
193-
* Lifetime parameters that appear anywhere in `A + ... + Z`, including elided lifetimes
189+
* Any generic type and const parameters in scope, including:
190+
* `Self`
191+
* Type and const parameters on the impl
192+
* Explicit type and const parameters on the method
193+
* Implicit type parameters on the method (argument-position `impl Trait` types)
194+
* Lifetime parameters that appear anywhere in the `impl A + ... + Z` type, including elided lifetimes
195+
196+
[scoping]: https://rust-lang.github.io/rfcs/1951-expand-impl-trait.html#scoping-for-type-and-lifetime-parameters
194197

195-
We say that a generic parameter is *captured* if it may appear in the hidden type. These rules are the same as those for `-> impl Trait` in inherent impls.
198+
Lifetime parameters not in scope may still be indirectly named by one of the type parameters in scope.
199+
200+
_Note_: The term "captured" is sometimes used as an alternative to "in scope".
196201

197202
When desugaring, captured parameters from the method are reflected as generic parameters on the `$` associated type. Furthermore, the `$` associated type brings whatever where clauses are declared on the method into scope, excepting those which reference parameters that are not captured.
198203

@@ -352,102 +357,6 @@ impl Trait for MyType {
352357

353358
Similarly, the equivalent `-> impl Future` signature in a trait can be satisfied by using `async fn` in an impl of that trait.
354359

355-
## Scoping rules for `impl Trait`
356-
357-
We say a generic parameter is "in scope" for an `impl Trait` type if the actual revealed type is allowed to name that parameter. The scoping rules for return position `impl Trait` in traits are the same as [those for return position `impl Trait` generally][scoping]. Specifically:
358-
359-
[scoping]: https://rust-lang.github.io/rfcs/1951-expand-impl-trait.html#scoping-for-type-and-lifetime-parameters
360-
361-
1. All types nameable at the site of the `impl Trait` are in scope, including argument-position `impl Trait` types.
362-
2. All lifetime parameters directly named in the `impl Trait` type are in scope.
363-
364-
Lifetime parameters not in scope may still be indirectly named by one of the type parameters in scope.
365-
366-
_Note_: The term "captured" is sometimes used as an alternative to "in scope".
367-
368-
### Implication for `async fn` in trait
369-
370-
`async fn` behaves [slightly differently][ref-async-captures] than return-position `impl Trait` when it comes to scoping rules. It considers _all_ lifetime parameters in-scope for the returned future.
371-
372-
[ref-async-captures]: https://doc.rust-lang.org/reference/items/functions.html#async-functions
373-
374-
In the case of there being one lifetime in scope (usually for `self`), the desugaring we've shown above is exactly equivalent:
375-
376-
```rust
377-
trait Trait {
378-
async fn async_fn(&self);
379-
}
380-
381-
impl Trait for MyType {
382-
fn async_fn(&self) -> impl Future<Output = ()> + '_ { .. }
383-
}
384-
```
385-
386-
It's worth taking a moment to discuss _why_ this works. The `+ '_` syntax here does two things:
387-
388-
1. It brings the lifetime of the `self` borrow into scope for the return type.
389-
2. It promises that the return type will outlive the borrow of `self`.
390-
391-
In reality, the second point is not part of the `async fn` desugaring, but it does not matter: We can already reason that because our return type has only one lifetime in scope, it must outlive that lifetime.[^OutlivesProjectionComponents]
392-
393-
[^OutlivesProjectionComponents]: After all, the return type cannot possibly reference any lifetimes *shorter* than the one lifetime it is allowed to reference. This behavior is specified as the rule `OutlivesProjectionComponents` in [RFC 1214](https://rust-lang.github.io/rfcs/1214-projections-lifetimes-and-wf.html#outlives-for-projections). Note that it only works when there are no type parameters in scope.
394-
395-
When there are multiple lifetimes however, writing an equivalent desugaring becomes awkward.
396-
397-
```rust
398-
trait Trait {
399-
async fn async_fn(&self, num: &u32);
400-
}
401-
```
402-
403-
We might be tempted to add another outlives bound:
404-
405-
```rust
406-
impl Trait for MyType {
407-
fn async_fn<'b>(&self, num: &'b u32) -> impl Future<Output = ()> + '_ + 'b { .. }
408-
}
409-
```
410-
411-
But this signature actually promises *more* than the original trait does, and would require `#[refine]`. The `async fn` desugaring allows the returned future to name both lifetimes, but does not promise that it *outlives* both lifetimes.[^intersection]
412-
413-
[^intersection]: Technically speaking, we can reason that the returned future outlives the *intersection* of all named lifetimes. In other words, when all lifetimes the future is allowed to name are valid, we can reason that the future must also be valid. But at the time of this RFC, Rust has no syntax for intersection lifetimes.
414-
415-
One way to get around this is to "collapse" the lifetimes together:
416-
417-
```rust
418-
impl Trait for MyType {
419-
fn async_fn<'a>(&'a self, num: &'a u32) -> impl Future<Output = ()> + 'a { .. }
420-
}
421-
```
422-
423-
In most cases[^lifetime-collapse] the type system actually recognizes these signatures as equivalent. This means it should be possible to write this trait with RPITIT now and move to async fn in the future. In the general case where these are not equivalent, it is possible to write an equivalent desugaring with a bit of a hack:
424-
425-
[^lifetime-collapse]: Both lifetimes must be [late-bound] and the type checker must be able to pick a lifetime that is the intersection of all input lifetimes, which is the case when either both are [covariant] or both are contravariant. The reason for this is described in more detail in [this comment](https://github.com/rust-lang/rust/issues/32330#issuecomment-202536977). In practice the equivalence can be checked [using the compiler](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=56faadfc236bb9acfb4af1b51a214a79). (Note that at the time of writing, a bug in the nightly compiler prevents it from accepting the example.)
426-
427-
[late-bound]: https://rust-lang.github.io/rfcs/0387-higher-ranked-trait-bounds.html#distinguishing-early-vs-late-bound-lifetimes-in-impls
428-
[covariant]: https://doc.rust-lang.org/reference/subtyping.html#variance
429-
430-
```rust
431-
trait Trait {
432-
async fn async_fn(&self, num_ref: &mut &u32);
433-
// ^^^^
434-
// The lifetime of this inner reference is invariant!
435-
}
436-
437-
impl Trait for MyType {
438-
// Let's say we do not want to use `async fn` here.
439-
// We cannot use the `+ 'a` syntax in this case,
440-
// so we use `Captures` to bring the lifetimes in scope.
441-
fn async_fn<'a, 'b>(&'a self, num_ref: &'a mut &'b u32)
442-
-> impl Future<Output = ()> + Captures<(&'a (), &'b ())> { .. }
443-
}
444-
445-
trait Captures<T> {}
446-
impl<T, U> Captures<T> for U {}
447-
```
448-
449-
The `Captures` trait doesn't promise anything at all; its sole purpose is to give you a place to name lifetime parameters you would like to be in scope for the return type. In the future we can provide a nicer syntax for dealing with these cases, or remove the difference in scoping rules altogether.
450-
451360
## Legal positions for `impl Trait` to appear
452361

453362
`impl Trait` can appear in the return type of a trait method in all the same positions as it can in a free function.
@@ -551,6 +460,93 @@ The `NewIntoIterator` trait used as an example in this RFC, however, doesn't sup
551460

552461
The [future possibilities](#future-possibilities) section discusses a planned extension to support naming the type returned by an impl trait, which could work to overcome this limitation for clients.
553462

463+
## Difference in scoping rules from `async fn`
464+
465+
`async fn` behaves [slightly differently][ref-async-captures] than return-position `impl Trait` when it comes to the scoping rules defined above. It considers _all_ lifetime parameters in-scope for the returned future.
466+
467+
[ref-async-captures]: https://doc.rust-lang.org/reference/items/functions.html#async-functions
468+
469+
In the case of there being one lifetime in scope (usually for `self`), the desugaring we've shown above is exactly equivalent:
470+
471+
```rust
472+
trait Trait {
473+
async fn async_fn(&self);
474+
}
475+
476+
impl Trait for MyType {
477+
fn async_fn(&self) -> impl Future<Output = ()> + '_ { .. }
478+
}
479+
```
480+
481+
It's worth taking a moment to discuss _why_ this works. The `+ '_` syntax here does two things:
482+
483+
1. It brings the lifetime of the `self` borrow into scope for the return type.
484+
2. It promises that the return type will outlive the borrow of `self`.
485+
486+
In reality, the second point is not part of the `async fn` desugaring, but it does not matter: We can already reason that because our return type has only one lifetime in scope, it must outlive that lifetime.[^OutlivesProjectionComponents]
487+
488+
[^OutlivesProjectionComponents]: After all, the return type cannot possibly reference any lifetimes *shorter* than the one lifetime it is allowed to reference. This behavior is specified as the rule `OutlivesProjectionComponents` in [RFC 1214](https://rust-lang.github.io/rfcs/1214-projections-lifetimes-and-wf.html#outlives-for-projections). Note that it only works when there are no type parameters in scope.
489+
490+
When there are multiple lifetimes however, writing an equivalent desugaring becomes awkward.
491+
492+
```rust
493+
trait Trait {
494+
async fn async_fn(&self, num: &u32);
495+
}
496+
```
497+
498+
We might be tempted to add another outlives bound:
499+
500+
```rust
501+
impl Trait for MyType {
502+
fn async_fn<'b>(&self, num: &'b u32) -> impl Future<Output = ()> + '_ + 'b { .. }
503+
}
504+
```
505+
506+
But this signature actually promises *more* than the original trait does, and would require `#[refine]`. The `async fn` desugaring allows the returned future to name both lifetimes, but does not promise that it *outlives* both lifetimes.[^intersection]
507+
508+
[^intersection]: Technically speaking, we can reason that the returned future outlives the *intersection* of all named lifetimes. In other words, when all lifetimes the future is allowed to name are valid, we can reason that the future must also be valid. But at the time of this RFC, Rust has no syntax for intersection lifetimes.
509+
510+
One way to get around this is to "collapse" the lifetimes together:
511+
512+
```rust
513+
impl Trait for MyType {
514+
fn async_fn<'a>(&'a self, num: &'a u32) -> impl Future<Output = ()> + 'a { .. }
515+
}
516+
```
517+
518+
In most cases[^lifetime-collapse] the type system actually recognizes these signatures as equivalent. This means it should be possible to write this trait with RPITIT now and move to async fn in the future. In the general case where these are not equivalent, it is possible to write an equivalent desugaring with a bit of a hack:
519+
520+
[^lifetime-collapse]: Both lifetimes must be [late-bound] and the type checker must be able to pick a lifetime that is the intersection of all input lifetimes, which is the case when either both are [covariant] or both are contravariant. The reason for this is described in more detail in [this comment](https://github.com/rust-lang/rust/issues/32330#issuecomment-202536977). In practice the equivalence can be checked [using the compiler](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=56faadfc236bb9acfb4af1b51a214a79). (Note that at the time of writing, a bug in the nightly compiler prevents it from accepting the example.)
521+
522+
[late-bound]: https://rust-lang.github.io/rfcs/0387-higher-ranked-trait-bounds.html#distinguishing-early-vs-late-bound-lifetimes-in-impls
523+
[covariant]: https://doc.rust-lang.org/reference/subtyping.html#variance
524+
525+
```rust
526+
trait Trait {
527+
async fn async_fn(&self, num_ref: &mut &u32);
528+
// ^^^^
529+
// The lifetime of this inner reference is invariant!
530+
}
531+
532+
impl Trait for MyType {
533+
// Let's say we do not want to use `async fn` here.
534+
// We cannot use the `+ 'a` syntax in this case,
535+
// so we use `Captures` to bring the lifetimes in scope.
536+
fn async_fn<'a, 'b>(&'a self, num_ref: &'a mut &'b u32)
537+
-> impl Future<Output = ()> + Captures<(&'a (), &'b ())> { .. }
538+
}
539+
540+
trait Captures<T> {}
541+
impl<T, U> Captures<T> for U {}
542+
```
543+
544+
Note that the `Captures` trait doesn't promise anything at all; its sole purpose is to give you a place to name lifetime parameters you would like to be in scope for the return type.
545+
546+
This difference is pre-existing, but it's worth highlighting in this RFC the implications for the adoption of this feature. If we stabilize this feature first, people will use it to emulate `async fn` in traits. Care will be needed not to create forward-compatibility hazards for traits that want to migrate to `async fn` later. The best strategy for someone in that situation might be to simulate such a migration with the nightly compiler.
547+
548+
We leave open the question of whether to stabilize these two features together. In the future we can provide a nicer syntax for dealing with these cases, or remove the difference in scoping rules altogether.
549+
554550
# Rationale and alternatives
555551
[rationale-and-alternatives]: #rationale-and-alternatives
556552

@@ -650,7 +646,7 @@ There are a number of crates that do desugaring like this manually or with proce
650646
# Unresolved questions
651647
[unresolved-questions]: #unresolved-questions
652648

653-
- None.
649+
- Should we stabilize this feature together with `async fn` to mitigate hazards of writing a trait that is not forwards-compatible with its desugaring? (See [drawbacks].)
654650

655651
# Future possibilities
656652
[future-possibilities]: #future-possibilities

0 commit comments

Comments
 (0)