Skip to content

Commit f012228

Browse files
committed
Extend discussion on use<..> impl Trait syntax
We had earlier written up a section on `use<..> impl Trait` syntax that mostly focused on why we had not adopted it. We didn't spend much text on why it's appealing, probably because we are in fact sympathetic to it and consider the reasons that it's appealing obvious. Still, we should write all those reasons down. Let's extend the section on this syntax with the best possible argument in favor. We also see more clearly now the fundamental intuitive tension behind this syntax and `impl use<..> Trait`, so let's write that down too. Finally, let's describe the historical and other factors that led to picking one syntax over the other. (Thanks to tmandry for suggesting the `use<..> impl Trait` syntax and many of the arguments in favor of it.)
1 parent 9fbce80 commit f012228

File tree

1 file changed

+104
-6
lines changed

1 file changed

+104
-6
lines changed

text/3617-precise-capturing.md

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -551,18 +551,76 @@ Decisive to some was that we may want this syntax to *scale* to other uses, most
551551

552552
Putting the `use<..>` specifier *before* the `impl` keyword is potentially appealing as `use<..>` applies to the entire `impl Trait` opaque type rather than to just one of the bounds, and this ordering might better suggest that.
553553

554-
However, this visual association might also *prove too much*. That is, it could make the `use<..>` look more like a *binder* (like `for<..>`) rather than like a *property* of the opaque type.
554+
Let's discuss some arguments for this, some arguments against it, and then discuss the fundamental tension here.
555555

556-
The `use<..>` syntax *applies* the listed generic *parameters* as generic *arguments* to the opaque type. It's analogous, e.g., with the generic arguments here:
556+
#### The case for `use<..>` before `impl`
557+
558+
We've been referring to the syntax for RPIT-like opaque types as `impl Trait`, as is commonly done. But this is a bit imprecise. The syntax is really `impl $bounds`. We might say, e.g.:
559+
560+
```rust
561+
fn foo() -> impl 'static + Unpin + for<'a> FnMut(&'a ()) {
562+
|_| ()
563+
}
564+
```
565+
566+
Each *bound*, separated by `+`, may be a *lifetime* or a *trait bound*. Each trait bound may include a higher ranked `for<..>` *binder*. The lifetimes introduced in such a binder are in scope only for the bound in which that binder appears.
567+
568+
This could create confusion with `use<..>` after `impl`. If we say, e.g.:
569+
570+
```rust
571+
fn foo<'a>(
572+
_: &'a (),
573+
) -> impl use<'a> for<'b> FnMut(&'b ()) + for<'c> Trait<'c> {
574+
// ^^^^^^^ ^^^^^^^ ^^^^^^^
575+
// | | ^ Applies to one bound.
576+
// | ^ Applies to one bound.
577+
// ^ Applies to the whole type.
578+
|_| ()
579+
}
580+
```
581+
582+
...then it may feel like `use<..>` should apply to only the first bound, just as the `for<..>` binder right next to it does. Putting `use<..>` *before* `impl` might avoid this issue. E.g.:
583+
584+
```rust
585+
fn foo<'a>(
586+
_: &'a (),
587+
) -> use<'a> impl for<'b> FnMut(&'b ()) + for<'c> Trait<'c> {
588+
|_| ()
589+
}
590+
```
591+
592+
This would make it clear that `use<..>` applies to the entire type. This seems the strongest argument for putting `use<..>` before `impl`, and it's a *good* one.
593+
594+
#### The case for and against `use<..>` before `impl`
595+
596+
There are some other known arguments for this ordering that may or may not resonate with the reader; we'll present these, along with the standard arguments that might be made in response, as an imagined conversation between Alice and Bob:
597+
598+
> **Bob**: We call the base feature here "`impl Trait`". Anything that we put between the `impl` and the `Trait` could make this less recognizable to people.
599+
>
600+
> **Alice**: Maybe, but users don't literally write the words `impl Trait`; they write `impl` and then a set of bounds. They could even write `impl 'static + Fn()`, e.g. The fact that there can be multiple traits and that a lifetime or a `for<..>` binder could come between the `impl` and the first trait doesn't seem to be a problem here, so maybe adding `use<..>` won't be either.
601+
>
602+
> **Bob**: But what about the orthography? In English, we might say "using 'x, we implement the trait". We'd probably try to avoid saying "we implement, using 'x, the trait". Putting `use<..>` first better lines up with this.
603+
>
604+
> **Alice**: Is that true? Would we always prefer the first version? To my ears, "using 'x, we implement the trait" sounds a bit like something Yoda would say. I'd probably say the second version, if I had to choose. Really, of course, I'd mostly try to say instead that "we implement the trait using 'x", but there are probably good reasons to not use that ordering here in Rust.
605+
>
606+
> **Bob**: The RFC talks about maybe later extending the `use<..>` syntax to closure-like blocks, e.g. `use<> |x| x`. If it makes sense to put the `use<..>` first here, shouldn't we put it first in `use<..> impl Trait`?
607+
>
608+
> **Alice**: That's interesting to think about. In the case of closure-like blocks, we'd probably want to put the `use<..>` in the same position as `move` as it could be extended to serve a similar purpose. For closures, that would mean putting it before the arguments, e.g. `use<> |x| x`, just as we do with `move`. But this would also imply that `use<..>` should appear *after* certain keywords, e.g. for `async` blocks we currently write `async move {}`, so maybe here we would write `async use<> {}`.
609+
>
610+
> **Alice**: There is a key difference to keep in mind here. Closure-like blocks are *expressions* but `impl Trait` is syntax for a *type*. We often have different conventions between type position and expression position in Rust. Maybe (or maybe not) this is a place where that distinction could matter.
611+
612+
#### The case against `use<..>` before `impl`
613+
614+
The `use<..>` specifier syntax *applies* the listed generic *parameters* as generic *arguments* to the opaque type. It's analogous, e.g., with the generic arguments here:
557615

558616
```rust
559617
impl Trait for () {
560618
type Opaque<'t, T> = Concrete<'t, T>
561-
// ^^^^^^^^|^^^^^
562-
// Type | Generic Arguments
619+
// ^^^^^^^^ ^^^^^
620+
// ^ Type ^ Generic arguments
563621
where Self: 'static;
564622
// ^^^^^^^^^^^^^
565-
// Bounds
623+
// ^ Bounds
566624
}
567625
```
568626

@@ -572,11 +630,51 @@ In the above example and throughout Rust, we observe the following order: *type*
572630

573631
This observation, that we're applying generic *arguments* to the opaque type and that the `impl` keyword is the stand-in for that type, is also a strong argument in favor of `impl<..> Trait` syntax. It's conceivable that we'll later, with more experience and consistently with [Stroustrup's Rule][], decide that we want to be more concise and adopt the `impl<..> Trait` syntax after all. One of the advantages of placing `use<..>` after `impl` is that there would be less visual and conceptual churn in later making that change.
574632

575-
Finally, there's one other practical advantage to placing `impl` before `use<..>`. If we were to do it the other way and place `use<..>` before `impl`, we would need to make a backward incompatible change to the `ty` macro matcher fragment specifier. This would require us to migrate this specifier according to our policy in [RFC 3531][]. This is something we could do, but it is a cost on us and on our users, and combined with the other good reasons that argue for `impl use<..> Trait` (or even `impl<..> Trait`), it doesn't seem a cost that's worth paying.
633+
Finally, there's one other practical advantage to placing `impl` before `use<..>`. If we were to do it the other way and place `use<..>` before `impl`, we would need to make a backward incompatible change to the `ty` macro matcher fragment specifier. This would require us to migrate this specifier according to our policy in [RFC 3531][]. This is something we could do, but it is a cost on us and on our users, even if only a modest one, and combined with the other good reasons that argue for `impl use<..> Trait` (or even `impl<..> Trait`), it doesn't seem a cost that's worth paying.
576634

577635
[RFC 3531]: https://github.com/rust-lang/rfcs/blob/master/text/3531-macro-fragment-policy.md
578636
[Stroustrup's Rule]: https://www.thefeedbackloop.xyz/stroustrups-rule-and-layering-over-time/
579637

638+
#### The fundamental tension on `impl use<..>` vs. `use<..> impl`
639+
640+
Throughout this RFC, we've given two intuitions for the semantics of `use<..>`:
641+
642+
- **Intuition #1**: `use<..>` *applies* generic arguments to the opaque type.
643+
- **Intuition #2**: `use<..>` brings generic parameters *into scope* for the hidden type.
644+
645+
These are *both* true and are both valid *intuitions*, but there's some tension between these for making this syntax choice.
646+
647+
It's often helpful to think of `impl Trait` in terms of generic associated types (GATs), and let's make that analogy here. Consider:
648+
649+
```rust
650+
impl Trait for () {
651+
type Opaque<'t, T> = Concrete<'t, T>;
652+
// ^^^^^^ ^^^^^ ^^^^^^^^ ^^^^^
653+
// | | | ^ Generic arguments applied
654+
// | | ^ Concrete type
655+
// | ^ Generic parameters introduced into scope
656+
// ^ Alias type (similar to an opaque type)
657+
fn foo<T>(&self) -> Self::Opaque<'_, T> { todo!() }
658+
// ^^^^^^^^^^^^ ^^^^^
659+
// ^ Alias type ^ Generic arguments applied
660+
}
661+
```
662+
663+
The question is, are the generics in `use<..>` more like the generic *parameters* or more like the generic *arguments* above?
664+
665+
If these generics are more like the generic *arguments* above (*Intuition #1*), then `impl<..> Trait` and `impl use<..> Trait` make a lot of sense as we're *applying* these arguments to the type. In Rust, when we're applying generic arguments to a type, the generic arguments appear *after* the type, and `impl` is the stand-in for the type here.
666+
667+
However, if these generics are more like the generic *parameters* above (*Intuition #2*), then `use<..> impl Trait` makes more sense. In Rust, when we're putting generic parameters into scope, they appear before the type.
668+
669+
Since both intuitions are valid, but each argues for a different syntax choice, picking one is tough. The authors are sympathetic to both choices. The key historical and tiebreaker factors leading to our choice of the `impl use<..> Trait` syntax in this RFC are:
670+
671+
- The original longstanding and motivating semantic intuition for this feature was *Intuition #1*, and it argues for this syntax. The second intuition, *Intuition #2*, was only developed in the process of writing this RFC and after most of this RFC had been written.
672+
- The `use<..> impl Trait` syntax was never proposed before this RFC was written (it may have been inspired by the presentation in this RFC of the second intuition), and in discussion, no clear consensus has yet emerged in its favor.
673+
- There are some practical costs that exist for `use<..> impl Trait` that don't for `impl use<..> Trait`.
674+
- The "obvious" syntax for this feature is `impl<..> Trait`. We may yet someday want to switch to this, and migrating from `impl use<..> Trait` seems like a smaller step.
675+
676+
We are *not* leaving this as an open question, because given that there have already been substantial and productive discussions on this topic, and given that it's a bit of a coin flip where we're likely to be happy at the end of the day with either choice, it seems better to just pick one. But all questions are in some sense open until stabilization, if feelings shift far enough and an alternate consensus emerges, and the authors hope that people will take the opportunity to experiment with and experience the syntax on nightly.
677+
580678
### `impl Trait & ..`
581679

582680
In some conceptions, the difference between `impl Trait + 'a + 'b` and `impl use<'a, 'b> Trait` is the difference between capturing the union of those lifetimes and capturing the intersection of them. This inspires syntax proposals such as `impl Trait & 't & T` or `impl Trait & ['t, T]` to express this intersection.

0 commit comments

Comments
 (0)