In the meeting we discussed the general state of the impl Trait
, with a particular focus on “what are the open questions that would prevent us from stabilizing?”
General status
@eddyb completed the implementation of the “most basic form” of impl Trait
. In particular, you can use impl Trait
in free function return types. This has already raised a couple of interesting issues that we didn’t uncover in the RFC process itself (covered below).
Looking forward, there are also a number of (relatively) straight-forward extensions that we’d like to see:
-
impl Trait
in trait method return types- this would effectively desugar to an associated type in the trait
- but this associated type would need to be generic, along the lines of RFC 1598
-
impl Trait
in argument position- this would be sugar for a generic argument
-
fn foo(x: impl FnMut())
becomesfn foo<T: FnMut()>(x: T)
-
- this raises syntactic questions (see below)
- this would be sugar for a generic argument
And other possible extensions that one could imagine, but where the desirability and semantics are less clear:
-
impl Trait
for local variables -
impl Trait
in structs
Question: what should be captured by an impl trait?
One of the more interesting questions that arose had to do with “capturing”. If you have a generic function, the current design allows the “hidden type” described by the impl Trait
to make use of any type variables it wishes. It turns out that this is not always what you probably want.
Let’s explore through an example. Consider https://is.gd/Cn6iAN. Here we have a function foo()
that returns a pair (T, U)
, though the caller only sees impl SomeTrait
:
fn foo<A: Debug, B: Debug>(a: A, b: B) -> impl SomeTrait {
(a, b)
}
The caller main()
can invoke foo
like so:
fn main() {
let mut i = 0;
let mut j = 0;
let k = foo(&i, &j);
// Are these legal?
// i += 1;
// j += 1;
k.print();
}
You’ll find that if you uncomment i += 1
or j += 1
, the program will not compile. This is because k
could potentially be using &i
and &j
, so we have to keep those two variables locked so long as k
is in scope. (And, in this case, it is.)
But of course the same is true regardless of the body of Foo
. So if we update Foo
to only use the first argument:
fn foo<A: Debug, B: Debug>(a: A, b: B) -> impl SomeTrait {
(a, ) // look ma, no `b`
}
we will still get the same errors in main
(and rightly so; we don’t want to leak internal impl details).
But sometimes we will want to take “transient” arguments that are not used in the return type! And this is particularly evident when those arguments are lifetime parameters. Imagine if instead of foo
we had something like this (lifetimes made explicit for clarity):
impl SomeType {
fn iter<'a,'b>(&'a self, config: &'b Config) -> impl Iterator<Item=Blah> { ... }
}
Now if you call x.iter(&config)
, this means that config
will be locked during the entire iteration. This is because we assume that the return type may “capture” 'b
.
Note the analogy to lifetime elision. If you have the same function signature, but you have a reference in the result:
impl SomeType {
fn iter(&self, config: &Config) -> &SomeConcreteIteratorType { ... }
}
Here lifetime elision would expand the return type to use the same lifetime as &self
. This is because experimentally we found that this is what you want most of the time. Note that we do not expand to the same lifetime in all three positions:
impl SomeType {
fn iter<'a>(&'a self, config: &'a Config) -> &'a SomeConcreteIteratorType { ... }
}
If we did so, then this would be more analogous to our impl Trait
behavior – i.e., this would mean that, by default, config
is locked as long as the result is used, just as is the case with impl Trait
.
So, the first thing we noted is that whatever we do, we will probably need some form of explicit syntax (just as with named lifetime parameters). No default will be right 100% of the time. So we propose the syntax impl<'a,B> Trait
where the 'a
and B
refer to lifetime or type parameters in scope that may be captured. If you don’t have any <>
(e.g., impl Trait
), then this applies the default (yet to be determined). If you have an empty <>
(e.g., impl<> Trait
), this implies no capturing at all. The fact that an empty <>
has a different meaning than no <>
gives some mild discomfort.
Next point is that we need to gather up data from what really happens in practice to determine the best default. It may be that the current behavior is best, but maybe not – or maybe we want a default that is different for type vs lifetime parameters. Thoughts?
Question: how to extend to argument position?
Most everyone in the lang team would like to see impl Trait
usable in argument position as a shorthand for declaring a type parameter. This is for several reasons. First, it’s a way to lighten notation significantly. Second, it may allow one to teach traits earlier without going the details of explicit parameters and parametric polymorphism. However, it does raise some questions. Because impl Trait
would be expanded differently in argument position, is it proper to use one keyword?
Many have argued for a distinction where the current impl Trait
(in return position) would be some Trait
, indicating that a specific type is being returned, even if it is not explicitly named, whereas the impl Trait
in argument position would be any Trait
. For example @withoutboats is a proponent.
On the other hand, it’s unclear how important this distinction will be in practice, and it could be confusing. An alternative would be to have impl Trait
that is a contextual shorthand, and then have explicit named parameter notation for both return position (which we lack today) and argument position (which we have). I prefer this, at least at present. =) This version does open the door that we might someday change the meaning of Trait
, though that opens up another can of worms best left out of this thread.
It’s worth noting that there are cases where one might want some Trait
even in argument position (and if we only had impl Trait
, we would not handle said cases correctly). An example would be fn foo(x: any FnMut(some Display))
, which is saying “give me a closure that is prepared to handle a type T: Display
but I’m not telling you what T
is”. @wycats has described this using the keyword “my”, which I think gives a better intution: fn foo(x: any FnMut(my Display)
, meaning "foo
gets to decide the type of Display
value you are given".
Clearly, before we stabilize impl Trait
notation, we should settle on whether we will want to use the keyword some
(or, no pun intended, some other alternative) instead.
For the record, I don’t like the keyword some
in particular because I think it is begging for confusing with the option variant Some
, also a new concept to many early Rust users.
What should block stabilization?
It seems clear that we have to settle on a keyword and capture semantics before we could stabilize, since any changes there would break code. But what about implementation status? How “complete” does the story have to be?
For example, @aturon felt like he would want at least argument position working before stabilizing, but that we didn’t have to have things working in traits. Otherwise the feature “feels too incomplete”. @nrc felt like he wants impl trait yesterday and would rather stabilizing bits and pieces as they come into being. This was the area where the meeting ended without a clear consensus.
Focus time!
So the key questions are:
- What do we need to fully settle before stabilizing
- default capture semantics
- keyword we want
- anything else?
- What do we need to implement before stabilizing
- obviously capture semantics must be right
- in particular, do we need argument position?
- trait position?
- anything else?