We've decided to add syntax for the precise capturing of generic parameters in RPIT-like impl Trait
opaque types. This solves the problem of overcapturing and will allow us to fully stabilize the Lifetime Capture Rules 2024 for RPIT in Rust 2024.
To make this work, we need to stabilize this syntax ahead of or along with the edition. To best support migration efforts, it would be better to stabilize this sooner rather than later.
To do that, we need to finalize the exact syntax. Based on our earlier discussions leading to the acceptance of RFC 3617, the key choice here is between:
impl use<..> Trait
use<..> impl Trait
The goal of this meeting is to pick which one of these we want.
To lay my own cards on the table, as the author, having now written out the arguments every which way, I find myself deeply sympathetic to the good arguments for both choices. As discussed below, in favor of impl use<..> Trait
is that it follows the natural ordering when applying generics as arguments to a type. In favor of use<..> impl Trait
is an appealing kind of syntactic principledness, in that it keeps use<..>
away from the bounds (which it is not a part of) in the same way that preferring super let pat = expr
to let super pat = expr
keeps super
away from the pattern (which it is not a part of), and is why the former was preferred by the draft RFC for super let
.
Mostly I think we're going to be happy either way. We'll just have to pick one.
Niko has been using this feature in nightly and recently reported his experience:
I thought I didn't care but within a few minutes of using it I found that
use<> impl
just felt much simpler to me. The best explanation I can give is that it feels less "stacked" – i.e., I can look at theuse
and process it as a kind of "prefix" on theimpl Trait
type, rather than being an inner part that I have to think carefully about. That combined with the different scoping relative tofor
feels like a very strong argument to me.
The remainder of the document is a detailed comparative analysis of these options drawn from the relevant alternatives section from RFC 3617.
use<..> impl Trait
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.
Let's discuss some arguments for this, some arguments against it, and then discuss the fundamental tension here.
use<..>
before impl
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.:
fn foo() -> impl 'static + Unpin + for<'a> FnMut(&'a ()) {
|_| ()
}
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.
This could create confusion with use<..>
after impl
. If we say, e.g.:
fn foo<'a>(
_: &'a (),
) -> impl use<'a> for<'b> FnMut(&'b ()) + for<'c> Trait<'c> {
// ^^^^^^^ ^^^^^^^ ^^^^^^^
// | | ^ Applies to one bound.
// | ^ Applies to one bound.
// ^ Applies to the whole type.
|_| ()
}
…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.:
fn foo<'a>(
_: &'a (),
) -> use<'a> impl for<'b> FnMut(&'b ()) + for<'c> Trait<'c> {
|_| ()
}
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.
use<..>
before impl
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:
Bob: We call the base feature here "
impl Trait
". Anything that we put between theimpl
and theTrait
could make this less recognizable to people.Alice: Maybe, but users don't literally write the words
impl Trait
; they writeimpl
and then a set of bounds. They could even writeimpl 'static + Fn()
, e.g. The fact that there can be multiple traits and that a lifetime or afor<..>
binder could come between theimpl
and the first trait doesn't seem to be a problem here, so maybe addinguse<..>
won't be either.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.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.
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 theuse<..>
first here, shouldn't we put it first inuse<..> impl Trait
?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 asmove
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 withmove
. But this would also imply thatuse<..>
should appear after certain keywords, e.g. forasync
blocks we currently writeasync move {}
, so maybe here we would writeasync use<> {}
.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.
use<..>
before impl
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:
impl Trait for () {
type Opaque<'t, T> = Concrete<'t, T>
// ^^^^^^^^ ^^^^^
// ^ Type ^ Generic arguments
where Self: 'static;
// ^^^^^^^^^^^^^
// ^ Bounds
}
Just as the above applies <'t, T>
to Concrete
, use<..>
applies its arguments to the opaque type.
In the above example and throughout Rust, we observe the following order: type, generic arguments (applied to the type), bounds. In impl Trait
syntax, the impl
keyword is the stand-in for the opaque type itself. The use<..>
specifier lists the generic arguments to be applied to that type. Then the bounds follow. Putting use<..>
after impl
is consistent with this rule, but the other way would be inconsistent.
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.
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.
impl use<..>
vs. use<..> impl
Throughout this RFC, we've given two intuitions for the semantics of use<..>
:
use<..>
applies generic arguments to the opaque type.use<..>
brings generic parameters into scope for the hidden type.These are both true and are both valid intuitions, but there's some tension between these for making this syntax choice.
It's often helpful to think of impl Trait
in terms of generic associated types (GATs), and let's make that analogy here. Consider:
impl Trait for () {
type Opaque<'t, T> = Concrete<'t, T>;
// ^^^^^^ ^^^^^ ^^^^^^^^ ^^^^^
// | | | ^ Generic arguments applied
// | | ^ Concrete type
// | ^ Generic parameters introduced into scope
// ^ Alias type (similar to an opaque type)
fn foo<T>(&self) -> Self::Opaque<'_, T> { todo!() }
// ^^^^^^^^^^^^ ^^^^^
// ^ Alias type ^ Generic arguments applied
}
The question is, are the generics in use<..>
more like the generic parameters or more like the generic arguments above?
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.
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.
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 2024-04-24 design meeting:
https://hackmd.io/PohGwog9SLK-XCg0ZWTpuQ
The 2024-04-03 planning meeting with the first bikeshed:
https://hackmd.io/c_-Fm49QRASsH7m-0VmtZw
scottmcm: this mentions the ty matcher, but can this really be used in every ty
position? What would it mean to have fn foo<...>(x: use<...> impl Trait)
, for example? Can I do impl Foo<...> for Bar<...> { type MyType = use<...> impl Trait; }
?
(I'm wondering if there's a more meaningful distinction here than just ty_2021
vs ty_2024
, like we have pat_param
vs pat
. Is there a ty_return
vs normal ty
, say?)
eholk: I don't immediately see why we couldn't, but I'd need to look more closely at the grammar.
NM: There are two questions. On is what we would parse. The other is what we accept semantically. We'd want to reject this in some places certainly.
eholk: As with other places, the macro matcher would probably accept it generally, but if it expands to a place where it can't be used, that would be an error. We have some precedent with how _
is matched by expr
.
scottmcm: There's also the precedent here of pat_param
vs pat
. If this can never show up in an argument and only in a return type… it makes me wonder if there's a possibility here to leave ty
alone and add ty_return
.
tmandry: Actually, I'm wondering why this would never be useful in argument position.
eholk: I like this idea of writing a ty_return
or whatnot. There are ergonomics considerations here. There are places we could end up with really precise matchers that aren't generally useful. But on the other hand, it'd be good to have confidence that expanding something matched would be legal in the intended place of use.
pnkfelix: These could also be used in type alias position. That might raise questions for that name.
pnkfelix: I'm confused by how this would be used in argument position. This seems deeply tied to opaque types.
NM: That's correct. I can maybe imagine future words in which it could have a meaning, but I'm not sure we'd go there. What this says in the abstract is that, "as long as this type is valid, these other types must also be valid, as far as the borrows go." But for argument generics, the types have to be valid throughout the function anyway.
fn foo<T, U>(arg: impl Debug) {
// becomes `fn foo<T, U, V>() where V: Debug`
}
fn foo<T, U>(arg: use<T> impl Debug) {
// ...also becomes `fn foo<T, U, V>() where V: Debug` ...?
}
NM: Proposal:
use
everywhere; semantically it is rejecteduse
use
ty
to ty_2021
impl use<..> + $bounds
Josh: When we considered syntax options before, did we consider:
impl use<'a, B> + Trait<C> + Other + ...
?
That could make the scope clearer, since it'd no longer look like use
applied only to the first term. "I return something that uses these lifetimes/parameters and implements this trait and …"
TC: We did not previously discuss this.
Josh: Then, if I'm understanding correctly how this is used, I'd like to propose this as being clearer. Effectively, rather than use<'a, B>
being a thing attached specifically to the impl
at the beginning of the construct (whether before or after it), it's one term in the impl X + Y + Z
construct. (Also, ideally that term could be placed anywhere in the list.)
NM: Similar to "the Captures
hack".
tmandry: This sort of works in the opposite direction from the other bounds. It's a weakening rather than a strengthening. I don't like that about it. The 2024 capture rules were trying to align the language with the correct intuition here, and this would seem to push against it.
NM:
fn foo<T, U>() -> impl PartialEq
// could capture everything (default: use<T, U>
)fn foo<T, U>() -> impl use<> + PartialEq
// captures nothingfn foo<T, U>() -> impl use<> + PartialEq<T>
// captures T
fn foo<T, U>() -> impl use<U> + PartialEq<T>
// captures T
, U
fn foo<T, U>() -> impl use<T, U> + PartialEq<T>
// captures T
, U
NM: use
here acts lke any other bound – adding use
means fewer types meet those bounds, but you can do more with it (it "outlasts" other things)
tmandry: For me, the bounds seem to restrict what you can do with a type…
NM: The bounds restricts the set of types but expands what you can do with it.
NM: Most bounds are about what you can call on it (e.g. the methods of a trait). There are bounds like 'static
.
NM: Where I went wrong here was saying that use<T> + use<U>
would be use<T, U>
. That's not true.
NM: If you have "at most one use", it works well.
JT: Yes, because use is a "subtraction from all" in some sense.
Intuition: use<B...>
is a trait that is implemented for all types that reference types in B...
.
TC: While I agree with what NM is saying, perhaps we can more precisely frame the intuition that tmandry has. As a list of bounds gets longer, it offers more to the caller. But as the list of arguments to use<..>
gets longer, it offers less to the caller. So the use<..>
construct is contravariant in its length as compared with the bounds.
NM: Yes, +1; it's contravariant. That's a great framing.
NM: My preference:
use
in the list right now and require that it includes all the identifiers from other boundsNM: There are places where I want to do something similar to this, such as with dyn
. So I think it's not bad that it scales. It may be comparable to the prefix use<..>
in that way.
scottmcm: It'd be interesting to be able to use these on GATs.
NM: Yes, it's interesting to think about these in a where clause.
Josh: E.g., where T: Debug + use<U>
.
NM: Yes, we'd make this part of the bounds list, it's just not a bound that could be used everywhere right now.
NM: GATs and opaque types are very related.
NM: We'd start with this where we need it right now, then we'd think about expanding it to e.g. where clauses on GATs.
fn foo<'a, T: 'a + Foo>(t: T) -> dyn Foo + 'a {
// Niko has always found this indirect and non-obvious...
Box::new(t)
}
fn foo<T>(t: T) -> dyn Foo + use<T> {
// ...I'd rather write this, though it could be `use<T> dyn Foo` just as well
Box::new(t)
}
NM: This also lets us put it at the end, which is nice. I like it at either the beginning or the end, just not where we had it at the beginning.
Josh: Yes, this isn't the most important thing to know about the return type, it's an added detail. The most important thing to know is usually the primary trait implemented. "This is a Future", not "Hey, before I tell you anything else, know that this uses 'a
".
NM:
fn foo<A, B, C>(
a: A,
b: B,
) -> impl Iterator + use<A, B> {
}
scottmcm: I've long wondered how we'd ever be able to do something like:
trait Iterator = StreamingIterator<Item<'a>: use<>>;
as a way to say it doesn't depend on the 'a
.
pnkfelix: Is there a current way to specify any kind of bounds on a closure expression directly, beyond the "return type" itself, e.g.:
fn foo<T>() -> impl Fn() {
// ~~~~~~~~~ this is the aforementioned "return type"
/* want to impose bounds on underlying type of the
closure expression below (presumably *not* by
annotating the return type above) */
|| { ... }
}
NM: It's sort of not surprising that we don't, as closures as expressions.
tmandry: we can still just do this…
fn foo() -> impl Fn() + use<> {
use<> || { .. }
}
tmandry: If we can use this for GATs, that's a strong argument.
scottmcm: If this can help us clean up dyn
, that would be really nice. +1 to that.
scottmcm: to Niko's point earlier, I agree that I'd love to rethink how we do bounds on trait objects. See "Consider re-tuning the lifetime elision rules for trait objects" https://github.com/rust-lang/rust/issues/91302. In particular, I think impl dyn Foo { … }
is too often a mistake, because people want impl dyn Foo + '_ { … }
.
NM: What are the disadvantages of putting it in the bounds list rather than putting it in front?
tmandry: The main one is the contravariance, using TC's framing there.
NM: My hunch is that this will not cause undue confusion. If you don't think too hard about it, it's OK.
TC: To pnkfelix's question earlier, note that this does work:
#![feature(precise_capturing)]
#![allow(incomplete_features)]
//@ edition: 2024
fn outlives<'o, T: 'o>(_: T) {}
fn foo<'a>(_: &'a ()) -> impl use<> Fn() {
// ^^^^^
// We would get an error below without this.
|| ()
}
fn main() {
let x = ();
outlives::<'static>(foo(&x));
}
NM: It seems OK for things to be a bit complicated as long as the compiler will point people in the right direction.
others: Agreed.
Josh: We could have rustfmt move use<..>
to the end.
scottmcm: The thing that comes to mind for me is Fn()
where it may be clearer to not have anything after it. Otherwise, I agree that at the end is probably fine most of the time.
fn foo() -> impl Fn(A) -> B + use<…>
// ^^^^^^^^^^ unclear to human
Exact proposal:
use<Parameter..>
.use<Parameter..>
appears at most once in the bound list.use
list (with machine-applicable suggestion to add those parameters to the use list).use
bound defines the set of parameters to appear on the hidden type
Pros:
Cons:
use
which is a bit odd
Consensus: We're agreed on the exact proposal above. We're interested in expanding this in the future to other places, such as the where clauses of GATs and to dyn
.
(This section of the meeting ended here.)
use
keywordtmandry: The framing of bringing parameters into scope vs applying arguments is helpful. I see it as bringing parameters into scope, and part of this is the fact that we are reusing the use
keyword. The whole purpose of use
directives is to bring an outside name into scope, and if we used it for closure captures I would frame its role in the same way.
Availability for planning:
Dates:
Availability:
Topics:
async fn next()
vs fn poll_next()
vs …) (note: libs-api inclined to punt to lang here)
We need tmandry for match ergonomics. Let's schedule a special meeting: