Tracking issue for `impl Trait` (RFC 1522, RFC 1951, RFC 2071)
aturon opened this issue · comments
NEW TRACKING ISSUE = #63066
Implementation status
The basic feature as specified in RFC 1522 is implemented, however there have been revisions that are still in need of work:
- impl Trait in argument position
- extend to cover traits
- support elision #49287
- impl Trait in return position is basically implemented, but:
- filter lifetimes
- code accepts types with out-of-scope lifetimes #46541
- ICEs when interacting with elision #43396
- ICE combining universal and existential #46685
- ICE with nested lifetimes #46464
- Infinitely recursive types not detected #38064
- do not accept nested impl trait described here (fixed in #48072)
-
let x: impl Trait
-
static
andconst T: impl Trait
-
abstract type
- preliminary refactoring possibility
- in modules
- in traits
RFCs
There have been a number of RFCs regarding impl trait, all of which are tracked by this central tracking issue.
- rust-lang/rfcs#1522
- the original, which covered only impl Trait in return position for inherent functions
- rust-lang/rfcs#1951
- settling on a particular syntax design, resolving questions around the some/any proposal and others.
- resolving questions around which type and lifetime parameters are considered in scope for an impl Trait.
- adding impl Trait to argument position.
- rust-lang/rfcs#2071
- named
abstract type
in modules and impls- Finalize syntax (rust-lang/rfcs#2515)
- Update RFC rust-lang/rfcs#2289 to match that syntax if that RFC gets merged.
- use of impl trait in
let
,const
, andstatic
positions
- named
- rust-lang/rfcs#2250
- Finalizing the syntax of
impl Trait
anddyn Trait
with multiple bounds
- Finalizing the syntax of
Unresolved questions
The implementation has raised a number of interesting questions as well:
- What is the precedence of the
impl
keyword when parsing types? Discussion: 1- e.g., how to associate
Send
forwhere F: Fn() -> impl Foo + Send
? - Resolved by rust-lang/rfcs#2250.
- Implemented (?) in #45294
- e.g., how to associate
- Should we allow
impl Trait
after->
infn
types or parentheses sugar? #45994 - Do we have to impose a DAG across all functions to allow for auto-safe leakage, or can we use some kind of deferral. Discussion: 1
- Present semantics: DAG.
- How should we integrate impl trait into regionck? Discussion: 1, 2
- Resolved in #45701
- Should we permit specifying types if some parameters are implicit and some are explicit? e.g.,
fn foo<T>(x: impl Iterator<Item = T>>)
?- Current behavior: An error to specify types
- Other alternatives: treat
impl Trait
as arguments in the list, permitting migration
- Some concerns about nested impl Trait usage
- Should the syntax in an impl be
existential type Foo: Bar
ortype Foo = impl Bar
? (see here for discussion)- Settled by rust-lang/rfcs#2515.
- Should the set of "defining uses" for an
existential type
in an impl be just items of the impl, or include nested items within the impl functions etc? (see here for example)
@aturon Can we actually put the RFC in the repository? (@mbrubeck commented there that this was a problem.)
Done.
First attempt at implementation is #35091 (second, if you count my branch from last year).
One problem I ran into is with lifetimes. Type inference likes to put region variables everywhere and without any region-checking changes, those variables don't infer to anything other than local scopes.
However, the concrete type must be exportable, so I restricted it to 'static
and explicitly named early-bound lifetime parameters, but it's never any of those if any function is involved - even a string literal doesn't infer to 'static
, it's pretty much completely useless.
One thing I thought of, that would have 0 impact on region-checking itself, is to erase lifetimes:
- nothing exposing the concrete type of an
impl Trait
should care about lifetimes - a quick search forReveal::All
suggests that's already the case in the compiler - a bound needs to be placed on all concrete types of
impl Trait
in the return type of a function, that it outlives the call of that function - this means that any lifetime is, by necessity, either'static
or one of the lifetime parameters of the function - even if we can't know which (e.g. "shortest of'a
and'b
") - we must choose a variance for the implicit lifetime parametrism of
impl Trait
(i.e. on all lifetime parameters in scope, same as with type parameters): invariance is easiest and gives more control to the callee, while contravariance lets the caller do more and would require checking that every lifetime in the return type is in a contravariant position (same with covariant type parametrism instead of invariant) - the auto trait leaking mechanism requires that a trait bound may be put on the concrete type, in another function - since we've erased the lifetimes and have no idea what lifetime goes where, every erased lifetime in the concrete type will have to be substituted with a fresh inference variable that is guaranteed to not be shorter than the shortest lifetime out of all actual lifetime parameters; the problem lies in the fact that trait impls can end up requiring stronger lifetime relationships (e.g.
X<'a, 'a>
orX<'static>
), which must be detected and errored on, as they can't be proven for those lifetimes
That last point about auto trait leakage is my only worry, everything else seems straight-forward.
It's not entirely clear at this point how much of region-checking we can reuse as-is. Hopefully all.
cc @rust-lang/lang
But lifetimes are important with impl Trait
- e.g.
fn get_debug_str(s: &str) -> impl fmt::Debug {
s
}
fn get_debug_string(s: &str) -> impl fmt::Debug {
s.to_string()
}
fn good(s: &str) -> Box<fmt::Debug+'static> {
// if this does not compile, that would be quite annoying
Box::new(get_debug_string())
}
fn bad(s: &str) -> Box<fmt::Debug+'static> {
// if this *does* compile, we have a problem
Box::new(get_debug_str())
}
I mentioned that several times in the RFC threads
trait-object-less version:
fn as_debug(s: &str) -> impl fmt::Debug;
fn example() {
let mut s = String::new("hello");
let debug = as_debug(&s);
s.truncate(0);
println!("{:?}", debug);
}
This is either UB or not depending on the definition of as_debug
.
@arielb1 Ah, right, I forgot that one of the reasons I did what I did was to only capture lifetime parameters, not anonymous late-bound ones, except it doesn't really work.
@arielb1 Do we have a strict outlives relation we can put between lifetimes found in the concrete type pre-erasure and late-bound lifetimes in the signature? Otherwise, it might not be a bad idea to just look at lifetime relationships and insta-fail any direct or indirect 'a outlives 'b
where 'a
is anything other than 'static
or a lifetime parameter and 'b
appears in the concrete type of an impl Trait
.
Sorry for taking a while to write back here. So I've been thinking
about this problem. My feeling is that we do, ultimately, have to (and
want to) extend regionck with a new kind of constraint -- I'll call it
an \in
constraint, because it allows you to say something like '0 \in {'a, 'b, 'c}
, meaning that the region used for '0
must be
either 'a
, 'b
, or 'c
. I'm not sure of the best way to integrate
this into solving itself -- certainly if the \in
set is a singleton
set, it's just an equate relation (which we don't currently have as a
first-class thing, but which can be composed out of two bounds), but
otherwise it makes things complicated.
This all relates to my desire to make the set of region constraints
more expressive than what we have today. Certainly one could compose a
\in
constraint out of OR
and ==
constraints. But of course more
expressive constraints are harder to solve and \in
is no different.
Anyway, let me just lay out a bit of my thinking here. Let's work with this
example:
pub fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {...}
I think the most accurate desugaring for a impl Trait
is probably a
new type:
pub struct FooReturn<'a, 'b> {
field: XXX // for some suitable type XXX
}
impl<'a,'b> Iterator for FooReturn<'a,'b> {
type Item = <XXX as Iterator>::Item;
}
Now the impl Iterator<Item=u32>
in foo
should behave the same as
FooReturn<'a,'b>
would behave. It's not a perfect match though. One
difference, for example, is variance, as eddyb brought up -- I am
assuming we will make impl Foo
-like types invariant over the type
parameters of foo
. The auto trait behavior works out, however.
(Another area where the match might not be ideal is if we ever add the
ability to "pierce" the impl Iterator
abstraction, so that code
"inside" the abstraction knows the precise type -- then it would sort
of have an implicit "unwrap" operation taking place.)
In some ways a better match is to consider a kind of synthetic trait:
trait FooReturn<'a,'b> {
type Type: Iterator<Item=u32>;
}
impl<'a,'b> FooReturn<'a,'b> for () {
type Type = XXX;
}
Now we could consider the impl Iterator
type to be like <() as FooReturn<'a,'b>>::Type
. This is also not a perfect match, because we
would ordinarily normalize it away. You might imagine using specialization
to prevent that though:
trait FooReturn<'a,'b> {
type Type: Iterator<Item=u32>;
}
impl<'a,'b> FooReturn<'a,'b> for () {
default type Type = XXX; // can't really be specialized, but wev
}
In this case, <() as FooReturn<'a,'b>>::Type
would not normalize,
and we have a much closer match. The variance, in particular, behaves
right; if we ever wanted to have some type that are "inside" the
abstraction, they would be the same but they are allowed to
normalize. However, there is a catch: the auto trait stuff doesn't
quite work. (We may want to consider harmonizing things here,
actually.)
Anyway, my point in exploring these potential desugarings is not to
suggest that we implement "impl Trait" as an actual desugaring
(though it might be nice...) but to give an intuition for our job. I
think that the second desugaring -- in terms of projections -- is a
pretty helpful one for guiding us forward.
One place that this projection desugaring is a really useful guide is
the "outlives" relation. If we wanted to check whether <() as FooReturn<'a,'b>>::Type: 'x
, RFC 1214 tells us that we can prove this
so long as 'a: 'x
and 'b: 'x
holds. This is I think how we want
to handle things for impl trait as well.
At trans time, and for auto-traits, we will have to know what XXX
is, of course. The basic idea here, I assume, is to create a type
variable for XXX
and check that the actual values which are returned
can all be unified with XXX
. That type variable should, in theory,
tell us our answer. But of course the problem is that this type
variable may refer to a lot of regions which are not in scope in the
fn signature -- e.g., the regions of the fn body. (This same problem
does not occur with types; even though, technically, you could put
e.g. a struct declaration in the fn body and it would be unnameable,
that's a kind of artificial restriction -- one could just as well move
the struct outside the fn.)
If you look both at the struct desugaring or the impl, there is an
(implicit in the lexical structure of Rust) restriction that XXX
can
only name either 'static
or lifetimes like 'a
and 'b
, which
appear in the function signature. That is the thing we are not
modeling here. I'm not sure the best way to do it -- some type
inference schemes have a more direct representation of scoping, and
I've always wanted to add that to Rust, to help us with closures. But
let's think about smaller deltas first I guess.
This is where the \in
constraint comes from. One can imagine adding
a type-check rule that (basically) FR(XXX) \subset {'a, 'b}
--
meaning that the "free regions" appearing in XXX can only be 'a
and
'b
. This would wind up translating to \in
requirements for the
various regions that appear in XXX
.
Let's look at an actual example:
fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
if condition { x.iter().cloned() } else { y.iter().cloned() }
}
Here, the type if condition
is true would be something like
Cloned<SliceIter<'a, i32>>
. But if condition
is false, we would
want Cloned<SliceIter<'b, i32>>
. Of course in both cases we would
wind up with something like (using numbers for type/region variables):
Cloned<SliceIter<'0, i32>> <: 0
'a: '0 // because the source is x.iter()
Cloned<SliceIter<'1, i32>> <: 0
'b: '1 // because the source is y.iter()
If we then instantiate the variable 0 to Cloned<SliceIter<'2, i32>>
,
we have '0: '2
and '1: '2
, or a total set of region relations
like:
'a: '0
'0: '2
'b: '1
'1: '2
'2: 'body // the lifetime of the fn body
So what value should we use for '2
? We have also the additional
constraint that '2 in {'a, 'b}
. With the fn as written, I think we
would have to report an error, since neither 'a
nor 'b
is a
correct choice. Interestingly, though, if we added the constraint 'a: 'b
, then there would be a correct value ('b
).
Note that if we just run the normal algorithm, we would wind up with
'2
being 'body
. I'm not sure how to handle the \in
relations
except for exhaustive search (though I can imagine some special
cases).
OK, that's as far as I've gotten. =)
On the PR #35091, @arielb1 wrote:
I don't like the "capture all lifetimes in the impl trait" approach and would prefer something more like lifetime elision.
I thought it would make more sense to discuss here. @arielb1, can you elaborate more on what you have in mind? In terms of the analogies I made above, I guess you are fundamentally talking about "pruning" the set of lifetimes that would appear either as parameters on the newtype or in the projection (i.e., <() as FooReturn<'a>>::Type
instead of <() as FooReturn<'a,'b>>::Type
or something?
I don't think that the lifetime elision rules as they exist would be a good guide in this respect: if we just picked the lifetime of &self
to include only, then we wouldn't necessarily be able to include the type parameters from the Self
struct, nor type parameters from the method, since they may have WF conditions that require us to name some of the other lifetimes.
Anyway, it'd be great to see some examples that illustrate the rules you have in mind, and perhaps any advantages thereof. :) (Also, I guess we would need some syntax to override the choice.) All other things being equal, if we can avoid having to pick from N lifetimes, I'd prefer that.
I haven't seen interactions of impl Trait
with privacy discussed anywhere.
Now fn f() -> impl Trait
can return a private type S: Trait
similarly to trait objects fn f() -> Box<Trait>
. I.e. objects of private types can walk freely outside of their module in anonymized form.
This seems reasonable and desirable - the type itself is an implementation detail, only its interface, available through a public trait Trait
is public.
However there's one difference between trait objects and impl Trait
. With trait objects alone all trait methods of private types can get internal linkage, they will still be callable through function pointers. With impl Trait
s trait methods of private types are directly callable from other translation units. The algorithm doing "internalization" of symbols will have to try harder to internalize methods only for types not anonymized with impl Trait
, or to be very pessimistic.
The "explicit" way to write foo
would be
fn foo<'a: 'c,'b: 'c,'c>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> + 'c {
if condition { x.iter().cloned() } else { y.iter().cloned() }
}
Here there is no question about the lifetime bound. Obviously, having to write the lifetime bound each time would be quite repetitive. However, the way we deal with that kind of repetition is generally through lifetime elision. In the case of foo
, elision would fail and force the programmer to explicitly specify lifetimes.
I am opposed to adding explicitness-sensitive lifetime elision as @eddyb did only in the specific case of impl Trait
and not otherwise.
@arielb1 hmm, I'm not 100% sure how to think about this proposed syntax in terms of the "desugarings" that I discussed. It allows you to specify what appears to be a lifetime bound, but the thing we are trying to infer is mostly what lifetimes appear in the hidden type. Does this suggest that at most one lifetime could be "hidden" (and that it would have to be specified exactly?)
It seems like it's not always the case that a "single lifetime parameter" suffices:
fn foo<'a, 'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
x.iter().chain(y).cloned()
}
In this case, the hidden iterator type refers to both 'a
and 'b
(although it is variant in both of them; but I guess we could come up with an example that is invariant).
So @aturon and I discussed this issue somewhat and I wanted to share. There are really a couple of orthogonal questions here and I want to separate them out. The first question is "what type/lifetime parameters can potentially be used in the hidden type?" In terms of the (quasi-)desugaring into a default type
, this comes down to "what type parameters appear on the trait we introduce". So, for example, if this function:
fn foo<'a, 'b, T>() -> impl Trait { ... }
would get desugared to something like:
fn foo<'a, 'b, T>() -> <() as Foo<...>>::Type { ... }
trait Foo<...> {
type Type: Trait;
}
impl<...> Foo<...> for () {
default type Type = /* inferred */;
}
then this question comes down to "what type parameters appear on the trait Foo
and its impl"? Basically, the ...
here. Clearly this include include the set of type parameters that appear are used by Trait
itself, but what additional type parameters? (As I noted before, this desugaring is 100% faithful except for the leakage of auto traits, and I would argue that we should leak auto traits also for specializable impls.)
The default answer we've been using is "all of them", so here ...
would be 'a, 'b, T
(along with any anonymous parameters that may appear). This may be a reasonable default, but it's not necessarily the best default. (As @arielb1 pointed out.)
This has an effect on the outlives relation, since, in order to determine that <() as Foo<...>>::Type
(referring to some particular, opaque instantiation of impl Trait
) outlives 'x
, we effectively must show that ...: 'x
(that is, every lifetime and type parameter).
This is why I say it is not enough to consider lifetime parameters: imagine that we have some call to foo
like foo::<'a0, 'b0, &'c0 i32>
. This implies that all three lifetimes, '[abc]0
, must outlive 'x
-- in other words, so long as the return value is in use, this will prolog the loans of all data given into the function. But, as @arielb1 poitned out, elision suggests that this will usually be longer than necessary.
So I imagine that what we need is:
- to settle on a reasonable default, perhaps using intution from elision;
- to have an explicit syntax for when the default is not appropriate.
@aturon spitballed something like impl<...> Trait
as the explicit syntax, which seems reasonable. Therefore, one could write:
fn foo<'a, 'b, T>(...) -> impl<T> Trait { }
to indicate that the hidden type does not in fact refer to 'a
or 'b
but only T
. Or one might write impl<'a> Trait
to indicate that neither 'b
nor T
are captured.
As for the defaults, it seems like having more data would be pretty useful -- but the general logic of elision suggests that we would do well to capture all the parameters named in the type of self
, when applicable. E.g., if you have fn foo<'a,'b>(&'a self, v: &'b [u8])
and the type is Bar<'c, X>
, then the type of self
would be &'a Bar<'c, X>
and hence we would capture 'a
, 'c
, and X
by default, but not 'b
.
Another related note is what the meaning of a lifetime bound is. I think that sound lifetime bounds have an existing meaning that should not be changed: if we write impl (Trait+'a)
that means that the hidden type T
outlives 'a
. Similarly one can write impl (Trait+'static)
to indicate that there are no borrowed pointers present (even if some lifetimes are captured). When inferring the hidden type T
, this would imply a lifetime bound like $T: 'static
, where $T
is the inference variable we create for the hidden type. This would be handled in the usual way. From a caller's perspective, where the hidden type is, well, hidden, the 'static
bound would allow us to conclude that impl (Trait+'static)
outlives 'static
even if there are lifetime parameters captured.
Here it just behaves exactly as the desugaring would behave:
fn foo<'a, 'b, T>() -> <() as Foo<'a, 'b, 'T>>::Type { ... }
trait Foo<'a, 'b, T> {
type Type: Trait + 'static; // <-- note the `'static` bound appears here
}
impl<'a, 'b, T> Foo<...> for () {
default type Type = /* something that doesn't reference `'a`, `'b`, or `T` */;
}
All of this is orthogonal from inference. We still want (I think) to add the notion of a "choose from" constraint and modify inference with some heuristics and, possibly, exhaustive search (the experience from RFC 1214 suggests that heuristics with a conservative fallback can actually get us very far; I'm not aware of people running into limitations in this respect, though there is probably an issue somewhere). Certainly, adding lifetime bounds like 'static
or 'a` may influence inference, and thus be helpful, but that is not a perfect solution: for one thing, they are visible to the caller and become part of the API, which may not be desired.
Possible options:
Explicit lifetime bound with output parameter elision
Like trait objects today, impl Trait
objects have a single lifetime bound parameter, which is inferred using the elision rules.
Disadvantage: unergonomic
Advantage: clear
Explicit lifetime bounds with "all generic" elision
Like trait objects today, impl Trait
objects have a single lifetime bound parameter.
However, elision creates a new early-bound parameters that outlives all explicit parameters:
fn foo<T>(&T) -> impl Foo
-->
fn foo<'total, T: 'total>(&T) -> impl Foo + 'total
Disadvantage: adds an early-bound parameter
more.
If I'm understanding this change correctly (and the chance of that is probably low!), then I think this playground code should work:
https://play.rust-lang.org/?gist=496ec05e6fa9d3a761df09c95297aa2a&version=nightly&backtrace=0
Both ThingOne
and ThingTwo
implement the Thing
trait. build
says it will return something that implements Thing
, which it does. Yet it does not compile. So I'm clearly misunderstanding something.
That "something" must have a type, but in your case you have two conflicting types. @nikomatsakis has previously suggested making this work in general by creating e.g. ThingOne | ThingTwo
as type mismatches appear.
@eddyb could you elaborate on ThingOne | ThingTwo
? Don't you need to have Box
if we only know the type at run-time? Or is it a kind of enum
?
Yeah it could be an ad-hoc enum
-like type that delegated trait method calls, where possible, to its variants.
I've wanted that kind of thing before too. The anonymous enums RFC: rust-lang/rfcs#1154
It's a rare case of something that works better if it's inference-driven, because if you only create these types on a mismatch, the variants are different (which is a problem with the generalized form).
Also you can get something out of not having pattern-matching (except in obviously disjoint cases?).
But IMO delegation sugar would "just work" in all relevant cases, even if you manage to get a T | T
.
Could you spell out the other, implicit halves of those sentences? I don't understand most of it, and suspect I'm missing some context. Were you implicitly responding to the problems with union types? That RFC is simply anonymous enums, not union types - (T|T)
would be exactly as problematic as Result<T, T>
.
Oh, nevermind, I got the proposals confused (also stuck on mobile until I sort out my failing HDD so apologies for sounding like on Twitter).
I find (positional, i.e T|U != U|T
) anonymous enums intriguing, and I believe they could be experimented with in a library if we had variadic generics (you can side-step this by using hlist
) and const generics (ditto, with peano numbers).
But, at the same time, if we had language support for something, it'd be union types, not anonymous enums. E.g. not Result
but error types (to bypass the tedium of named wrappers for them).
I am not sure whether this is the righ place to ask, but why is a keyword like impl
needed? I could not find a discussion (could be my fault).
If a function returns impl Trait, its body can return values of any type that implements Trait
Since
fn bar(a: &Foo) {
...
}
means "accept a reference to a type that implements trait Foo
" i would expect
fn bar() -> Foo {
...
}
to mean "return a type that implements trait Foo
". Is this impossible?
@kud1ing the reason is to not remove the possibility of having a function that returns the dynamically sized type Trait
if support for dynamically sized return values is added in the future. Currently Trait
is already a valid DST, it's just not possible to return a DST so you need to box it to make it a sized type.
EDIT: There is some discussion about this on the linked RFC thread.
Well, for one, regardless of whether dynamically sized return values will be added, I prefer the current syntax. Unlike what happens with trait objects, this isn't type erasure, and any coincidences like "parameter f: &Foo
takes something that impls Foo
, whereas this returns something that impls Foo
" could be misleading.
I gathered from RFC discussion that right now impl
is a placeholder implementation, and no impl
is very much desired. Is there any reason for not wanting an impl
Trait if the return value is not DST?
I think the current impl technique for handling "auto trait leakage" is problematic. We should instead enforce a DAG ordering so that if you define a fn fn foo() -> impl Iterator
, and you have a caller fn bar() { ... foo() ... }
, then we have to type-check foo()
before bar()
(so that we know what the hidden type is). If a cycle results, we'd report an error. This is a conservative stance -- we can probably do better -- but I think the current technique, where we collect auto-trait obligations and check them at the end, does not work in general. For example, it would not work well with specialization.
(Another possibility that might be more permissive than requiring a strict DAG is to type-check both fns "together" to some extent. I think that is something to consider only after we have re-archicted the trait system impl a bit.)
@Nercury I don't understand. Are you asking if there are reasons to not want fn foo() -> Trait
to mean -> impl Trait
?
@nikomatsakis Yes, I was asking precisely that, sorry for convulted language :). I thought that doing this without impl keyword would be simpler, because this behavior is exactly what one would expect (when a concrete type is returned in place of trait return type). However, I might be missing something, that's why I was asking.
The difference is that functions returning impl Trait
always return the same type- it's basically return type inference. IIUC, functions returning just Trait
would be able to return any implementation of that trait dynamically, but the caller would need to be prepared to allocate space for the return value via something like box foo()
.
@Nercury The simple reason is that the -> Trait
syntax already has a meaning, so we have to use something else for this feature.
I've actually seen people expect both kinds of behavior by default, and this sort of confusion comes up often enough I'd honestly rather that fn foo() -> Trait
not mean anything (or be a warning by default) and there were explicit keywords for both the "some type known at compile time that I get to choose but the caller doesn't see" case and the "trait object that could be dynamically dispatching to any type implementing Trait" case, e.g. fn foo() -> impl Trait
vs fn foo() -> dyn Trait
. But obviously those ships have sailed.
Why doesn't the compiler generate an enum that holds all the different return types of the function, implements the trait passing though the arguments to each variant, and returns that instead?
That would bypass the only one return type allowed-rule.
@NeoLegends Doing this manually is fairly common, and some sugar for it might be nice and has been proposed in the past, but it's a third completely different set of semantics from returning impl Trait
or a trait object, so it's not really relevant to this discussion.
@Ixrec Yeah I know this is being done manually, but the real use case of the anonymous enums as compiler generated return types is types that you cannot spell out, like long chains of iterator or future adaptors.
How is this different semantics? Anonymous enums (as far as the compiler generates them, not as per the anonymous enums RFC) as return values only really make sense if there is a common API like a trait that abstracts away the different variants. I'm suggesting a feature that still looks like and behaves like the regular impl Trait, just with the one-type-limit removed through a compiler generated enum the consumer of the API will never see directly. The consumer should always only see 'impl Trait'.
Anonymous, auto-generated enums gives impl Trait
a hidden cost that's easy to miss, so that's something to consider.
I suspect the "auto enum pass-through" thing only makes sense for object-safe traits. Is the same thing true of impl Trait
itself?
@rpjohnst Unless this the actual method variant is in crate metadata and monomorphised at the call site. Of course, this requires that change from one variant to another does not break the caller. And this might be too magical.
I suspect the "auto enum pass-through" thing only makes sense for object-safe traits. Is the same thing true of impl Trait itself?
this is an interesting point! I have been debating what is the right way to "desugar" impl trait, and was actually on the verge of suggesting that maybe we wanted to think of it more as a "struct with a private field" as opposed to the "abstract type projection" interpretation. However, that seems to imply something much like generalized newtype deriving, which of course was famously found to be unsound in Haskell when combined with type families. I confess to not having a full understanding of this unsoundness "in cache" but it seems like we would have to be very cautious here whenever we want to automatically generate an implementation of a trait for some type F<T>
from an impl for T
.
The problem is, in Rust terms
trait Foo {
type Output;
fn get() -> Self::Output;
}
fn foo() -> impl Foo {
// ...
// what is the type of return_type::get?
}
The tl;dr is that generalized newtype deriving was (and is) implemented by simply transmute
ing the vtable -- after all, a vtable consists of functions on the type, and a type and its newtype have the same representation, so should be fine, right? But it breaks if those functions also use types which are determined by type-level branching on the identity (rather than representation) of the given type -- e.g., using type functions or associated types (or in Haskell, GADTs). Because there's no guarantee that the representations of those types are also compatible.
Note that this problem is only possible because of the use of unsafe transmute. If it instead just generated the boring boilerplate code to wrap/unwrap the newtype everywhere and dispatch every method to its implementation from the base type (like some of the automatic delegation proposals for Rust IIRC?), then the worst possible outcome would be a type error or maybe an ICE. After all, by construction, if you do not use unsafe code you cannot have an unsafe outcome. Likewise, if we generated code for some kind of "automatic enum passthrough", but didn't use any unsafe
primitives to do so, there wouldn't be any danger.
(I'm not sure whether or how this relates to my original question of whether the traits used with impl Trait
, and/or automatic enum passthrough, by necessity would have to be object-safe, though?)
@rpjohnst One could make the enum case opt-in to mark the cost:
fn foo() -> enum impl Trait { ... }
That's almost certainly food for a different RFC though.
@glaebhoerl yeah I spent some time digging into the issue and felt fairly convinced it would not be a problem here, at least.
Apologies if it's something obvious but I'm trying to understand the reasons why impl Trait
can't appear in return types of trait methods, or whether it makes sense at all in the first place? E.g.:
trait IterInto {
type Output;
fn iter_into(&self) -> impl Iterator<Item=impl Into<Self::Output>>;
}
@aldanor It totally makes sense, and AFAIK the intention is to make that work, but it hasn't been implemented yet.
It sort of makes sense, but it's not same underlying feature (this has been discussed a lot btw):
// What that trait would desugar into:
trait IterInto {
type Output;
type X: Into<Self::Output>;
type Y: Iterator<Item=Self::X>;
fn iter_into(&self) -> Self::Y;
}
// What an implementation would desugar into:
impl InterInto for FooList {
type Output = Foo;
// These could potentially be left unspecified for
// a similar effect, if we want to allow that.
type X = impl Into<Foo>;
type Y = impl Iterator<Item=Self::X>;
fn iter_into(&self) -> Self::Y {...}
}
Specifically, impl Trait
in the impl Trait for Type
associated types' RHSes would be similar to the feature implemented today, in that it can't be desugared to stable Rust, whereas in the trait
it can be.
I know this is probably both too late, and mostly bikeshedding, but has it been documented anywhere why the keyword impl
was introduced? It seems to me like we already have a way in current Rust code to say "the compiler figures out what type goes here", namely _
. Could we not re-use this here to give the syntax:
fn foo() -> _ as Iterator<Item=u8> {}
@jonhoo That's not what the feature does, the type is not the one returned from the function, but rather a "semantic wrapper" that hides everything except the chosen APIs (and OIBITs because those are a pain).
We could allow some functions to infer types in their signatures by forcing a DAG, but such a feature has never been approved and it's unlikely to ever be added to Rust, as it'd be touching on "global inference".
Suggest the use of @Trait
syntax to replace impl Trait
, as mentioned here.
It is easier to extend to other type positions and in composition like Box<@MyTrait>
or &@MyTrait
.
@Trait
for any T where T: Trait
and ~Trait
for some T where T: Trait
:
fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> ~Fn(T) -> V {
move |x| g(f(x))
}
In fn func(t: T) -> V
, no need to distinguish any t or some v, so as trait.
fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> @Fn(T) -> V {
move |x| g(f(x))
}
still works.
@J-F-Liu I am personally opposed to having any
and some
conflated into one keyword/sigil but you are technically correct that we could have a single sigil and use it like the original impl Trait
RFC.
@
is also use in pattern matching, not removed from the language.
The thing I had in mind is that AFAIK sigils were over-used.
Syntax bikesheding: I am deeply unhappy about impl Trait
notation, because using a keyword (bold font in an editor) to name a type is way too loud. Remember C's struct
and Stroustroup loud syntax observation(slide 14)?
In https://internals.rust-lang.org/t/ideas-for-making-rust-easier-for-beginners/4761, @konstin suggested <Trait>
syntax. It looks really nice, especially in the input positions:
fn take_iterator(iterator: <Iterator<Item=i32>>)
I see that it will somewhat conflict with UFCS, but maybe this can be worked out?
I too feel using angle brackets instead of impl Trait to be a better choice, at least in return type position, e.g.:
fn returns_iter() -> <Iterator<Item=i32>> {...}
fn returns_closure() -> <FnOnce() -> bool> {...}
<Trait>
syntax conflicts with generics, consider:
Vec<<FnOnce() -> bool>>
vs Vec<@FnOnce() -> bool>
If Vec<FnOnce() -> bool>
is allowed, then <Trait>
is good idea, it signifies the equivalence to generic type parameter. But since Box<Trait>
is different to Box<@Trait>
, have to give up <Trait>
syntax.
I prefer the impl
keyword syntax because when you read documentation rapidly this allow less way to misread prototypes.
What do you think ?
I'm just realising I did propose a superset to this rfc in the internals thread (Thanks for @matklad for pointing me here):
Allow traits in function parameters and return types to be used by surrounding them with angle brackets like in the following example:
fn transform(iter: <Iterator>) -> <Iterator> {
// ...
}
The compiler would then monomorphise the parameter using the same rules currently applied to generics. The return type could e.g. be derived from the functions implementation. This does mean that you can't simply call this method on a Box<Trait_with_transform>
or using it on dynamically dispatched objects in general, but it would still make the rules more permissive. I haven't read through all of the RFC discussion, so maybe there's a better solution already there I've missed.
I prefer the impl keyword syntax because when you read documentation rapidly this allow less way to missread prototypes.
A different color in the syntax highlighting should do the trick.
This paper by Stroustrup discusses similar syntactic choices for C++ concepts in section 7: http://www.stroustrup.com/good_concepts.pdf
Do not use the same syntax for generics and existentials. They are not the same thing. Generics allow the caller to decide what the concrete type is, while (this restricted subset of) existentials allows the function being called to decide what the concrete type is. This example:
fn transform(iter: <Iterator>) -> <Iterator>
should either be equivalent to this
fn transform<T: Iterator, U: Iterator>(iter: T) -> U
or it should be equivalent to this
fn transform(iter: impl Iterator) -> impl Iterator
The last example won't compile correctly, even on nightly, and it's not actually callable with the iterator trait, but a trait like FromIter
would allow the caller to construct an instance and pass it to the function without being able to determine the concrete type of what they're passing.
Maybe the syntax should be similar, but it should not be the same.
No need to distinguish any of (generics) or some of (existentials) in type name, it depends on where the type is used. When used in variables, arguments and struct fields always accept any of T, when used in fn return type always get some of T.
- use
Type
,&Type
,Box<Type>
for concrete data types, static dispatch - use
@Trait
,&@Trait
,Box<@Trait>
and generic type parameter for abstract data type, static dispatch - use
&Trait
,Box<Trait>
for abstract data type, dynamic dispatch
fn func(x: @Trait)
is equivalent to fn func<T: Trait>(x: T)
.
fn func<T1: Trait, T2: Trait>(x: T1, y: T2)
can be simply written as fn func(x: @Trait, y: @Trait)
.
T
paramter is still needed in fn func<T: Trait>(x: T, y: T)
.
struct Foo { field: @Trait }
is equivalent to struct Foo<T: Trait> { field: T }
.
When used in variables, arguments and struct fields always accept any of T, when used in fn return type always get some of T.
You can return any-of-Trait, right now, in stable Rust, using the existing generic syntax. It's a very heavily used feature. serde_json::de::from_slice
takes &[u8]
as a parameter and returns T where T: Deserialize
.
You can also meaningfully return some-of-Trait, and that's the feature we're discussing. You can not use existentials for the deserialize function, just like you can't use generics to return unboxed closures. They're different features.
For a more familiar example, Iterator::collect
can return any T where T: FromIterator<Self::Item>
, implying my preferred notation: fn collect(self) -> any FromIterator<Self::Item>
.
How about the syntax
fn foo () -> _ : Trait { ... }
for return values and
fn foo (m: _1, n: _2) -> _ : Trait where _1: Trait1, _2: Trait2 { ... }
for parameters?
To me really none of the new suggestions come close to impl Trait
in it's elegancy. impl
is a keyword already known to every rust programmer and since it's used for implementing traits it actually suggests what the feature is doing just on its own.
Yeah, sticking with existing keywords seems ideal to me; I'd like to see impl
for existentials and for
for universals.
I am personally opposed to having
any
andsome
conflated into one keyword/sigil
@eddyb I wouldn’t consider it a conflation. It follows naturally from the rule:
((∃ T . F⟨T⟩) → R) → ∀ T . (F⟨T⟩ → R)
Edit: it’s one-way, not an isomorphism.
Unrelated: Is there any related proposal to also allow impl Trait
in other covariant positions such as
fn foo<F, R>(callback: F) -> R
where F: FnOnce(impl SomeTrait) -> R {
callback(create_something())
}
Right now, this is not a necessary feature, since you can always put a concrete time for impl SomeTrait
, which hurts readability but is otherwise not a big deal.
But if RFC 1522 feature stabilizes, then it would be impossible to assign a type signature to programs such the above if create_something
results in impl SomeTrait
(at least without boxing it). I think this is problematic.
@Rufflewind In the real world, things aren't so clear-cut, and this feature is a very specific brand of existentials (Rust has several by now).
But even then, all you have there is the use of covariance to determine what impl Trait
means inside and outside function arguments.
That's not enough for:
- using the opposite of the default
- disambiguating inside a field's type (where both
any
andsome
are equally desirable)
@Rufflewind That seems like the wrong bracketing for what impl Trait
is. I know Haskell exploits this relationship to use only the forall
keyword to represent both universals and existentials, but it doesn't work out in the context we're discussing.
Take this definition, for example:
fn foo(x: impl ArgTrait) -> impl ReturnTrait { ... }
If we use the rule that "impl
in arguments is universal, impl
in return types is existential", then the type of the foo
function item type is logically this (in made-up type notation):
forall<T: ArgTrait>(exists<R: ReturnTrait>(fn(T) -> R))
Naively treating impl
as technically only meaning universal or only meaning existential and letting the logic work itself out doesn't actually work out. You would get either this:
forall<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)
Or this:
exists<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)
And neither of these reduce to what we want by logical rules. So ultimately any
/some
do capture an important distinction you can't capture with a single keyword. There are even reasonable examples in std
where you want universals in return position. For instance, this Iterator
method:
fn collect<B>(self) -> B where B: FromIterator<Self::Item>;
// is equivalent to
fn collect(self) -> any FromIterator<Self::Item>;
And there is no way to write it with impl
and the argument/return rule.
tl;dr having impl
contextually denote either universal or existential really does give it two distinct meanings.
For reference, in my notation the forall/exists relationship @Rufflewind mentioned looks like:
fn(exists<T: Trait>(T)) -> R === forall<T: Trait>(fn(T) -> R)
Which is related to the concept of trait objects (existentials) being equivalent to generics (universals), but not to this impl Trait
question.
That said, I'm not strongly in favour of any
/some
anymore. I wanted to be precise about what we're talking about, and any
/some
have this theoretical and visual niceness, but I would be fine with using impl
with the contextual rule. I think it covers all the common cases, it avoids contextual keyword grammar issues, and we can drop to named type parameters for the rest.
On that note, to match the full generality of universals, I think we'll eventually need a syntax for named existentials, which enables arbitrary where clauses and the ability to use the same existential in multiple places in the signature.
In summary, I'd be happy with:
impl Trait
as the shorthand for both universals and existentials (contextually).- Named type parameters as the fully general longhand for both universals and existentials. (Less commonly necessary.)
Naively treating impl as technically only meaning universal or only meaning existential and letting the logic work itself out doesn't actually work out. You would get either this:
@solson To me, a “naive” translation would result in the existential quantifiers right next to the type being quantified. Hence
(impl MyTrait)
is just syntactic sugar for
(exists<T: MyTrait> T)
which is a simple local transformation. Thus, a naive translation obeying the “impl
is always an existential” rule would result in:
fn(exists<T: ArgTrait> T) -> (exists<R: ReturnTrait> R)
Then, if you pull the quantifier out out of the function argument, it becomes
for<T: ArgTrait> fn(T) -> (exists<R: ReturnTrait> R)
So even though T
is always existential relative to itself, it appears as universal relative to the whole function type.
IMO, I think impl
may as well become the de facto keyword for existential types. In the future, perhaps one could conceivably construct more complicated existential types like:
(impl<T: MyTrait> (Vec<T>, T))
in analogy to universal types (via HRTB)
(for<'a> FnOnce(&'a T))
@Rufflewind That view doesn't work because fn(T) -> (exists<R: ReturnTrait>(R))
isn't logically equivalent to exists<R: ReturnTrait>(fn(T) -> R)
, which is what return-type impl Trait
really means.
(At least not in the constructive logic usually applied to type systems, where the specific witness chosen for an existential is relevant. The former implies the function could choose different types to return based on, say, the arguments, while the latter implies there is one specific type for all invocations of the function, as is the case in impl Trait
.)
I feel that we are getting a bit far afield, as well. I think contextual impl
is an okay compromise to make, and I don't think reaching for this kind of justification is necessary or particularly helpful (we certainly wouldn't teach the rule in terms of this kind of logical connection).
@solson Yeah you’re right: existentials can’t be floated out. This one does not hold in general:
(T → ∃R. f(R)) ⥇ ∃R. T → f(R)
whereas these do hold in general:
(∃R. T → f(R)) → T → ∃R. f(R)
(∀A. g(A) → T) ↔ ((∃A. g(A)) → T)
The last one is responsible for the re-interpretation of existentials in arguments as generics.
Edit: Oops, (∀A. g(A) → T) → (∃A. g(A)) → T
does hold.
I've posted an RFC with a detailed proposal to expand and stabilize impl Trait
. It draws on a lot of the discussion on this and earlier threads.
Worth noting that rust-lang/rfcs#1951 has been accepted.
What's the status on this currently? We have an RFC that landed, we have people using the initial implementation, but I'm not clear on what items are todo.
It was found in #43869 that -> impl Trait
function does not support a purely diverging body:
fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
unimplemented!()
}
Is this expected (since !
does not impl Iterator
), or considered a bug?
What about defining inferred types, that could not only be used as return values, but as anything(i guess) a type can be used for currently?
Something like:
type Foo: FnOnce() -> f32 = #[infer];
Or with a keyword:
infer Foo: FnOnce() -> f32;
The type Foo
could then be used as a return type, parameter type or anything else a type can be used for, but it would be illegal to use it on two different places that require a different type, even if that type implements FnOnce() -> f32
in both cases. For example, the following would not compile:
infer Foo: FnOnce() -> f32;
fn return_closure() -> Foo {
|| 0.1
}
fn return_closure2() -> Foo {
|| 0.2
}
fn main() {
println!("{:?}, {:?}", return_closure()(), return_closure2()());
}
This shouldn't compile because even tho the return types from return_closure
and return_closure2
are both FnOnce() -> f32
, their types are actually different, because no two closures have the same type in Rust. For the above to compile you would thus need to define two different inferred types:
infer Foo: FnOnce() -> f32;
infer Foo2: FnOnce() -> f32; //Added this line
fn return_closure() -> Foo {
|| 0.1
}
fn return_closure2() -> Foo2 { //Changed Foo to Foo2
|| 0.2
}
fn main() {
println!("{:?}, {:?}", return_closure()(), return_closure2()());
}
I think what's happening here is quite obvious after seeing the code, even if you didn't know beforehand what the infer keyword does, and it is very flexible.
The infer keyword (or macro) would essentially tell the compiler to figure out what the type is, based on where it is used. If the compiler is not able to infer the type, it would throw an error, this could happen when there is not enough information to narrow down what type it has to be(if the inferred type isn't used anywhere, for example, although maybe it is better to make that specific case a warning), or when it is impossible to find a type that fits everywhere it is used(like in the example above).
So, @cramertj was asking me about how I thought it would be best to resolve the problem of late-bound regions that they encountered in their PR. My take is that we probably want to "retool" our implementation a bit to try and look forward to the anonymous type Foo
model.
For context, the idea is roughly that
fn foo<'a, 'b, T, U>() -> impl Debug + 'a
would be (sort of) desugared to something like this
anonymous type Foo<'a, T, U>: Debug + 'a
fn foo<'a, 'b, T, U>() -> Foo<'a, T, U>
Note that in this form, you can see which generic parameters are captured because they appear as arguments to Foo
-- notably, 'b
is not captured, because it does not appear in the trait reference in any way, but the type parameters T
and U
always are.
Anyway, at present in the compiler, when you have a impl Debug
reference, we create a def-id that -- effectively -- represents this anonymous type. Then we have the generics_of
query, which computes its generic parameters. Right now, this returns the same as the "enclosing" context -- that is, the function foo
. This is what we want to change.
On the "other side", that is, in the signature of foo
, we represent impl Foo
as a TyAnon
. This is basically right -- the TyAnon
represents the reference to Foo
that we see in the desugaring above. But the way that we get the "substs" for this type is to use the "identity" function, which is clearly wrong -- or at least doesn't generalize.
So in particular there is a kind of "namespace violation" taking place here. When we generate the "identity" substs for an item, that normally gives us the substitutions we would use when type-checking that item -- that is, with all its generic parameters in scope. But in this case, we creating the reference to Foo
that appears inside of the function foo()
, and so we want to have the generic parameters of foo()
appearing in Substs
, not those of Foo
. This happens to work because right now those are one and the same, but it's not really right.
I think what we should be doing is something like this:
First, when we compute the generic type parameters of Foo
(that is, the anonymous type itself), we would begin constructing a fresh set of generics. Naturally it would include the types. But for lifetimes, we would walk over the trait bounds and identify each of the regions that appear within. That is very similar to this existing code that cramertj wrote, except we don't want to accumulate def-ids, because not all regions in scope have def-ids.
I think what we want to be doing is accumulating the set of regions that appear and putting them in some order, and also tracking the values for those regions from the point-of-view of foo()
. It's a bit annoying to do this, because we don't have a uniform data structure that represents a logical region. (We used to have the notion of FreeRegion
, which would almost have worked, but we don't use FreeRegion
for early-bound stuff anymore, only for late-bound stuff.)
Perhaps the easiest and best option would be to just use a Region<'tcx>
, but you'd have to shift the debruijn index depths as you go to "cancel out" any binders that got introduced. This is perhaps the best choice though.
So basically as we get callbacks in visit_lifetime
, we would transform those into a Region<'tcx>
expressed in the initial depth (we'll have to track as we pass through binders). We'll accumulate those into a vector, eliminating duplicates.
When we're done, we have two things:
- First, for each region in the vector, we need to create a generic region parameter. They can all have anonymous names or whatever, it doesn't much matter (though maybe we need them to have def-ids or something...? I have to look at the
RegionParameterDef
data structures...). - Second, the regions in the vector are also the things we want to use for the "substs".
OK, sorry if that is a cryptic. I can't quite figure out how to say it more clearly. Something I'm not sure of though -- right now, I feel that our handling of regions is pretty complex, so maybe there is a way to refactor things to make it more uniform? I would bet $10 that @eddyb has some thoughts here. ;)
@nikomatsakis I believe a lot of that is similar to what I've told @cramertj, but more fleshed out!
I've been thinking about existential impl Trait
and I encountered a curious case where I think we should proceed with caution. Consider this function:
trait Foo<T> { }
impl Foo<()> for () { }
fn foo() -> impl Foo<impl Debug> {
()
}
As you can validate on play, this code compiles today. However, if we dig into what is happening, it highlights something that has a "fowards compatibility" danger that concerns me.
Specifically, it's clear how we deduce the type that is being returned here (()
). It's less clear how we deduce the type of the impl Debug
parameter. That is, you can think of this return value as being something like -> ?T
where ?T: Foo<?U>
. We have to deduce the values of ?T
and ?U
based just on the fact that ?T = ()
.
Right now, we do this by leveraging the fact that there exists only one impl. However, this is a fragile property. If a new impl is added, the code will no longer compile, because now we cannot uniquely determine what ?U
must be.
This can happen in lots of scenarios in Rust -- which is concerning enough, but orthogonal -- but there is something different about the impl Trait
case. In the case of impl Trait
, we don't have a way for user's to add type annotations to guide the inference along! Nor do we really have a plan for such a way. The only solution is to change the fn interface to impl Foo<()>
or something else explicit.
In the future, using abstract type
, one could imagine allowing users to explicitly give the hidden value (or perhaps just incomplete hints, using _
), which could then help inference along, while keeping roughly the same public interface
abstract type X: Debug = ();
fn foo() -> impl Foo<X> {
()
}
Still, I think it would be prudent to avoid stabilizing "nested" uses of existential impl Trait, except for in associated type bindings (e.g., impl Iterator<Item = impl Debug>
does not suffer from these ambiguities).
In the case of impl Trait, we don't have a way for user's to add type annotations to guide the inference along! Nor do we really have a plan for such a way.
Perhaps it could look like UFCS? e.g. <() as Foo<()>>
-- not changing the type like a bare as
, just disambiguating it. This is currently invalid syntax as it expects ::
and more to follow.
I just found an interesting case regarding type inference with impl Trait for Fn
:
The following code compiles just fine:
fn op(s: &str) -> impl Fn(i32, i32) -> i32 {
match s {
"+" => ::std::ops::Add::add,
"-" => ::std::ops::Sub::sub,
"<" => |a,b| (a < b) as i32,
_ => unimplemented!(),
}
}
If we comment out the Sub-line, a compile error is thrown:
error[E0308]: match arms have incompatible types
--> src/main.rs:4:5
|
4 | / match s {
5 | | "+" => ::std::ops::Add::add,
6 | | // "-" => ::std::ops::Sub::sub,
7 | | "<" => |a,b| (a < b) as i32,
8 | | _ => unimplemented!(),
9 | | }
| |_____^ expected fn item, found closure
|
= note: expected type `fn(_, _) -> <_ as std::ops::Add<_>>::Output {<_ as std::ops::Add<_>>::add}`
found type `[closure@src/main.rs:7:16: 7:36]`
note: match arm with an incompatible type
--> src/main.rs:7:16
|
7 | "<" => |a,b| (a < b) as i32,
| ^^^^^^^^^^^^^^^^^^^^
error: aborting due to previous error
@oberien This doesn't seem related to impl Trait
-- it's true of inference in general. Try this slight modification of your example:
fn main() {
let _: i32 = (match "" {
"+" => ::std::ops::Add::add,
//"-" => ::std::ops::Sub::sub,
"<" => |a,b| (a < b) as i32,
_ => unimplemented!(),
})(5, 5);
}
Looks like this is closed now:
ICEs when interacting with elision
One thing that I don't see listed in this issue or in the discussion is the ability to store closures and generators – that aren't provided by the caller – in struct fields. Right now, this is possible but it looks ugly: you have to add a type parameter to the struct for each closure/generator field, and then in the constructor function's signature, replace that type parameter with impl FnMut/impl Generator
. Here is an example, and it works, which is pretty cool! But it leaves a lot to be desired. It would be way better if you could get rid of the type parameter:
struct Counter(impl Generator<Yield=i32, Return=!>);
impl Counter {
fn new() -> Counter {
Counter(|| {
let mut x: i32 = 0;
loop {
yield x;
x += 1;
}
})
}
}
impl Trait
may not be the right way to do this – probably abstract types, if I've read and understood RFC 2071 correctly. What we need is something that we can write in the struct definition so that the actual type ([generator@src/main.rs:15:17: 21:10 _]
) can be inferred.
@mikeyhew abstract types would indeed be the way we expect that to work, I believe. The syntax would look roughly like
abstract type MyGenerator: Generator<Yield = i32, Return = !>;
pub struct Counter(MyGenerator);
impl Counter {
pub fn new() -> Counter {
Counter(|| {
let mut x: i32 = 0;
loop {
yield x;
x += 1;
}
})
}
}
Is there a fallback path if it's someone else's impl Generator
that I want to put in my struct, but they didn't make an abstract type
for me to use?
@scottmcm You can still declare your own abstract type
:
// library crate:
fn foo() -> impl Generator<Yield = i32, Return = !> { ... }
// your crate:
abstract type MyGenerator: Generator<Yield = i32, Return = !>;
pub struct Counter(MyGenerator);
impl Counter {
pub fn new() -> Counter {
let inner: MyGenerator = foo();
Counter(inner)
}
}
@cramertj Wait, abstract types are already in nightly?! Where's the PR?
@alexreg No, they are not.
Edit: Greetings, visitors from the future! The issue below has been resolved.
I'd like to call attention to this funky edge case of usage that appears in #47348
use ::std::ops::Sub;
fn test(foo: impl Sub) -> <impl Sub as Sub>::Output { foo - foo }
Should returning a projection on impl Trait
like this even be allowed? (because currently, it is.)
I couldn't locate any discussion about usage like this, nor could I find any test cases for it.
@ExpHP Hmm. It does seem problematic, for the same reason that impl Foo<impl Bar>
is problematic. Basically, we don't have any real constraint on the type in question -- only on the things projected out from it.
I think we want to reuse the logic around "constrained type parameters" from impls. In short, specifying the return type should "constrain" the impl Sub
. The function I am referring to is this one:
rust/src/librustc_typeck/constrained_type_params.rs
Lines 89 to 93 in a0dcecf
@rfcbot fcp merge
I propose that we stabilize the conservative_impl_trait
and universal_impl_trait
features, with one pending change (a fix to #46541).
Tests that document current semantics
The tests for these features can be found in the following directories:
run-pass/impl-trait
ui/impl-trait
compile-fail/impl-trait
Questions Resolved During Implementation
The details of parsing of impl Trait
were resolved in RFC 2250 and implemented in #45294.
impl Trait
has been banned from nested-non-associated-type position and certain qualified path positions in order to prevent ambiguity. This was implemented in #48084.
Remaining Unstable Features
After this stabilization, it will be possible to use impl Trait
in argument position and return position of non-trait functions. However, the use of impl Trait
anywhere in Fn
syntax is still disallowed in order to allow for future design iteration. Additionally, manually specifying the type parameters of functions which use impl Trait
in argument position is not allowed.
Team member @cramertj has proposed to merge this. The next step is review by the rest of the tagged teams:
No concerns currently listed.
Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!
See this document for info about what commands tagged team members can give me.