Rust: Tracking issue for `impl Trait` (RFC 1522, RFC 1951, RFC 2071)

Created on 27 Jun 2016  ·  417Comments  ·  Source: rust-lang/rust

NEW TRACKING ISSUE = https://github.com/rust-lang/rust/issues/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:

RFCs

There have been a number of RFCs regarding impl trait, all of which are tracked by this central tracking issue.

Unresolved questions

The implementation has raised a number of interesting questions as well:

B-RFC-implemented B-unstable C-tracking-issue T-lang disposition-merge finished-final-comment-period

Most helpful comment

Since this is the last chance before FCP closes, I'd like to make one last argument against automatic auto traits. I realize this is a bit last minute, so at most I'd like to formally address this issue before we commit to the current implementation.

To clarify for anyone who hasn't been following impl Trait, this is the issue I'm presenting. A type represented by impl X types currently automatically implement auto traits if and only if the concrete type behind them implements said auto traits. Concretely, if the following code change is made, the function will continue to compile, but any usages of the function relying on the fact that the type it returns implements Send will fail.

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(simpler example: working, internal changes causes failure)

This issue is not clear cut. There was a very delibrate decision to have auto traits "leak": if we didn't, we'd have to put + !Send + !Sync on every function which returns something non-Send or non-Sync, and we'd have an unclear story with potential other custom auto-traits which could simply not be implementable on the concrete type the function is returning. These are two problems I will touch on later.

First, I'd like to simply state my objection to the issue: this allows changing a function body to change the public facing API. This directly reduces the maintainability of code.

Throughout the development of rust, decisions have been made which err on the side of verbosity over usability. When newcomers see these, they think it's verbosity for the sake of verbosity, but this is not the case. Each decision, whether it's to not have structures automatically implement Copy, or to have all types explicit at function signatures, is for the sake of maintainability.

When I introduce people to Rust, sure, I can show them speed, productivity, memory safety. But go has speed. Ada has memory safety. Python has productivity. What Rust has trumps all of these, it has maintainability. When a library author wants to change an algorithm to be more efficient, or when they want to redo the structure of a crate, they have a strong guarantee from the compiler that it will tell them when they make mistakes. In rust, I can be assured that my code will continue to function not just in terms of memory safety, but logic and interface as well. _Every function interface in Rust is fully representable by the function's type declaration_.

Stabilizing impl Trait as is has a large chance of going against this belief. Sure, it's extremely nice for the sake of writing code quickly, but if I want to prototype I'll use python. Rust is the language of choice when one needs long-term maintainability, not short-term write-only code.


I say there's only a "large chance" of this being bad here because again, the issue isn't clear cut. The whole idea of 'auto traits' in the first place is non-explicit. Send and Sync are implemented based on what the contents of a struct, not the public declaration. Since this decision worked out for rust, impl Trait acting similarly could also work out well.

However, functions and structures are used differently in a codebase, and these aren't the same issues.

When modifying the fields of a structure, even private fields, it's immediately clear one's changing the real contents of it. Structures with non-Send or non-Sync fields made that choice, and library maintainers know to double check when a PR changes a structure's fields.

When modifying the internals of a function, it's definitely clear one can affect both performance and correctness. However, in Rust, we don't need to check that we're returning the right type. Function declarations are a hard contract we must uphold, and rustc watches our back. It's a thin line between auto traits on structs and in function returns, but changing the internals of a function is much more routine. Once we have full generator-powered Futures, it will be even more routine to modify functions returning -> impl Future. These will all be changes authors need to screen for modified Send/Sync implementations if the compiler doesn't catch it.

To resolve this, we could decide that this is an acceptable maintenance burden, as the original RFC discussion did. This section in the conservative impl trait RFC lays out the biggest arguments for leaking auto traits ("OIBIT"s being the old name for auto traits).

I've already laid out my main response to this, but here's one last note. Changing a structure's layout is not that common to do; it can be guarded against. The maintenance burden in ensuring functions continue to implement the same auto traits is greater than that of structures simply because functions change a lot more.


As a final note, I'd like to say that automatic auto traits is not the only option. It's the option we chose, but the alternative of opt-out auto traits is still an alternative.

We could require functions returning non-Send / non-Sync items to either state + !Send + !Sync or to return a trait (alias possibly?) which has those bounds. This wouldn't be a good decision, but it might be better than the one we're currently choosing.

As for the concern regarding custom auto traits, I would argue that any new auto traits should be not-implemented only for new types introduced after the auto trait. This might provide more of an issue than I can address now, but it's not one we can't address with more design.


This is very late, and very long winded, and I'm certain I've raised these objections before. I'm glad just to be able to comment one last time, and ensure we're fully OK with the decision we're making.

Thank you for reading, and I hope that the final decision sets Rust in the best direction it can go in.

All 417 comments

@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 for Reveal::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> or X<'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

@eddyb

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 Traits 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.

@nikomatsakis

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.

I ran into this issue with impl Trait +'a and borrowing: https://github.com/rust-lang/rust/issues/37790

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: https://github.com/rust-lang/rfcs/pull/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.

@glaebhoerl

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.

@nikomatsakis

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 transmuteing 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.

@J-F-Liu @eddyb There was a reason sigils were removed from language. Why that reason wouldn't apply to this case?

@ 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 and some 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

~rust
fn foo(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 and some 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

~rust
(impl MyTrait)
~

is just syntactic sugar for

~rust
(exists T)
~

which is a simple local transformation. Thus, a naive translation obeying the “impl is always an existential” rule would result in:

~rust
fn(exists T) -> (exists R)
~

Then, if you pull the quantifier out out of the function argument, it becomes

~rust
for fn(T) -> (exists 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:

~~rust
(impl (Vec, T))
~
~

in analogy to universal types (via HRTB)

~rust
(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 https://github.com/rust-lang/rfcs/pull/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).

@cramertj Ahh so that's why this issue had gotten so silent..

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:

https://github.com/rust-lang/rust/blob/a0dcecff90c45ad5d4eb60859e22bb3f1b03842a/src/librustc_typeck/constrained_type_params.rs#L89-L93

Tiny bit of triage for people who like checkboxes:

  • #46464 is done -> checkbox
  • #48072 is done -> checkbox

@rfcbot fcp merge

I propose that we stabilize the conservative_impl_trait and universal_impl_trait features, with one pending change (a fix to https://github.com/rust-lang/rust/issues/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 https://github.com/rust-lang/rust/pull/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 https://github.com/rust-lang/rust/pull/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:

  • [x] @aturon
  • [x] @cramertj
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @withoutboats

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.

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.

What is the status of using impl Trait in argument/return positions in trait functions, or in Fn syntax, for that matter?

@alexreg Return-position impl Trait in traits is blocked on an RFC, although RFC 2071 will allow similar functionality once implemented. Argument-position impl Trait in traits isn't blocked on any technical features that I'm aware of, but it wasn't explicitly permitted in the RFC so it has been omitted for the time being.

impl Trait in the argument position of Fn syntax is blocked on type-level HRTB, since some folks think that T: Fn(impl Trait) should desugar to T: for<X: Trait> Fn(X). impl Trait in the return position of Fn syntax isn't blocked for any technical reason that I'm aware of, but it was disallowed in the RFC pending further design work-- I would expect to see another RFC or at least a separate FCP before stabilizing this.

@cramertj Okay, thanks for the update. Hopefully we can see these two features that aren't blocked on anything get the go-ahead soon, after some discussion. The desugaring makes sense, in the argument position, an argument foo: T where T: Trait is equivalent to foo: impl Trait, unless I'm mistaken.

Concern: https://github.com/rust-lang/rust/issues/34511#issuecomment-322340401 is still the same. Is it possible to allow the following?

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

@kennytm No, that's not possible at the moment. That function returns !, which does not implement the trait you provided, nor do we have a mechanism to convert it to an appropriate type. This is unfortunate, but there isn't an easy way to fix it right now (aside from implementing more traits for !). It's also backwards-compatible to fix in the future, as making it work would allow strictly more code to compile.

The turbofish question has only been halfway resolved. We should at least warn on impl Trait in arguments of effectively public functions, considering impl Trait in arguments to be a private type for the new private in public check.

The motivation is to prevent libs from breaking users' turbofishes by changing an argument from explicit generic to impl Trait. We don't yet have a good reference guide for libs to know what is and is not a breaking change and it's very unlikely that tests will catch this. This issue was not sufficiently discussed, if we wish to stabilize before fully deciding we should at least point the gun away from the foot of lib authors.

The motivation is to prevent libs from breaking users' turbofishes by changing an argument from explicit generic to impl Trait.

I hope when this starts happening and people start complaining lang-team people who are in currently doubt will be convinced that impl Trait should support explicitly supplying type arguments with turbofish.

@leodasvacas

The turbofish question has only been halfway resolved. We should at least warn on impl Trait in arguments of effectively public functions, considering impl Trait in arguments to be a private type for the new private in public check.

I disagree- this has been resolved. We are disallowing turbofish for these functions completely for the time being. Changing the signature of a public function to use impl Trait instead of explicit generic parameters is a breaking change.

If we do allow turbofish for these functions in the future, it will likely only allow specifying non-impl Trait type parameters.

:bell: This is now entering its final comment period, as per the review above. :bell:

I should add that I don't want to stabilize until https://github.com/rust-lang/rust/pull/49041 lands. (But hopefully that'll be soon.)

So #49041 contains a fix for #46541, but that fix has more impact than I anticipated -- e.g., the compiler doesn't bootstrap now -- and it's giving me a measure of pause about the right course here. The problem in #49041 is that we could accidentally allow lifetimes to leak out that we were not supposed to. Here is how this manifests in the compiler. We might have a method like this:

impl TyCtxt<'cx, 'gcx, 'tcx>
where 'gcx: 'tcx, 'tcx: 'cx
{
    fn foos(self) -> impl Iterator<Item = &'tcx Foo> + 'cx {
        /* returns some type `Baz<'cx, 'gcx, 'tcx>` that captures self */
    }
}

The key thing here is that TyCtxt is invariant w/r/t 'tcx and 'gcx, so they must appear in the return type. And yet only 'cx and 'tcx appear in the impl trait bounds, so only those two lifetimes are supposed to be "captured". The old compiler was accepting this because 'gcx: 'cx, but that's not really correct if you think about the desugaring we have in mind. That desugaring would create an abstract type like this:

abstract type Foos<'cx, 'tcx>: Iterator<Item = &'tcx Foo> + 'cx;

and yet the value for this abstract type would be Baz<'cx, 'gcx, 'tcx> -- but 'gcx is not in scope!

The workaround here is that we have to name 'gcx in the bounds. This is kind of annoying to do; we can't use 'cx + 'gcx. We can I suppose make a dummy trait:

trait Captures<'a> { }
impl<T: ?Sized> Captures<'a> for T { }

and then return something like this impl Iterator<Item = &'tcx Foo> + Captures<'gcx> + Captures<'cx>.

Something I forgot to note: if the declared return type were dyn Iterator<Item = &'tcx Foo> + 'cx, that would be ok, because dyn types are expected to erase lifetimes. Therefore i don't believe any unsoundness is possible here, presuming you can't do anything problematic with an impl Trait that would not be possible with a dyn Trait.

One could vaguely imagine the idea that the value of the abstract type is a similar existential: exists<'gcx> Baz<'cx, 'gcx, 'tcx>.

It seems to me however ok to stabilize a conservative subset (which rules out the fns above) and revisit this as a possible expansion later, once we've decided how we want to think about it.

UPDATE: To clarify my meaning about dyn traits: I am saying that they can "hide" a lifetime like 'gcx so long as the bound ('cx, here) ensures that 'gcx will still be live wherever the dyn Trait is used.

@nikomatsakis That is an interesting example, but I don't feel it changes the basic calculus here, i.e. that we want all relevant lifetimes to be clear from the return type alone.

The Captures trait seems like a good, lightweight approach for this situation. It seems like it could go into std::marker as unstable for the moment?

@nikomatsakis Your follow-up comment made me realize I hadn't quite put all the pieces together here in understanding why you might expect to elide the 'gcx in this case, i.e. 'gcx is not a "relevant lifetime" from the client's point of view. In any case, starting conservative seems fine.

My personal opinion is that https://github.com/rust-lang/rust/issues/46541 isn't really a bug-- it's the behavior that I would expect, I don't see how it could be made unsound, and it's a pain to work around. IMO it should be possible to return a type which implements Trait and outlives lifetime 'a as impl Trait + 'a, no matter what other lifetimes it contains. However, I'm fine with stabilizing a more conservative approach to start if that's what @rust-lang/lang prefers.

(One other thing to clarify: the only time you will get errors with the fix in #49041 is when the hidden type is invariant with respect to the missing lifetime 'gcx, so this probably strikes relatively rarely.)

@cramertj

My personal opinion is that #46541 isn't really a bug-- it's the behavior that I would expect, I don't see how it could be made unsound, and it's a pain to work around.

I am sympathetic to that POV, but I'm reluctant to stabilize something for which we don't understand how to desugar it (e.g., because it seems to rely on some vague notion of existential lifetimes).

@rfcbot concern multiple-return-sites

I would like to register one final concern with existential impl trait. A substantial fraction of the times that I want to use impl trait, I actually want to return more than one type. For example:

fn foo(empty: bool) -> impl Iterator<Item = u32> {
    if empty { None.into_iter() } else { &[1, 2, 3].cloned() }
}

Of course, this doesn't work today, and it's definitely out of scope to make it work. However, the way that impl trait works now, we are effectively closing the door for it to ever work (with that syntax). This is because -- currently -- you can accumulate constraints from multiple return sites:

fn foo(empty: bool) -> (impl Debug, impl Debug) {
    if empty { return (22, Default::default()); }
    return (Default::default(), false);
}

Here, the inferred type is (i32, bool), where first return constrains the i32 part, and the second return constrains bool part.

This implies that we could never support cases where the two return statements do not unify (as in my first example) -- or else it would be very annoying to do so.

I wonder if we should put in a constraint that requires that each return (in general, each source of a constraint) must be independently fully specified? (And we unify them after the fact?)

This would make my second example illegal, and leave room for us to potentially support the first case at some point in the future.

@rfcbot resolve multiple-return-sites

So I talked to @cramertj on #rust-lang a fair bit. We were discussing the idea of making "early return" unstable for impl Trait, so that we might eventually change it.

They made the case that it would be better to have an explicit opt-in to this sort of syntax, specifically because there are other cases (e.g., let x: impl Trait = if { ... } else { ... }) where one would want it, and we can't expect to handle them all implicitly (definitely not).

I find this pretty persuasive. Prior to this, I was assuming we would have some opt-in syntax here anyway, but I just wanted to be sure we didn't close any doors prematurely. After all, explaining when you need to insert the "dynamic shim" is kind of tricky.

@nikomatsakis Just my possibly less-informed opinion: While enabling a function to return one of multiple types possible at run-time can be useful, I would be reluctant to have the same syntax for both static return type inferrence to a single type, and allowing situations where some run-time decision is required internally (what you just called the "dynamic shim").

That first foo example, as far as I understood the problem, could either resolve to (1) a boxed + type-erased Iterator<Item = u32>, or (2) a sum type of std::option::Iter or std::slice::Iter, which in turn would derive an Iterator implementation. Trying to keep it short, since there were some updates on the discussion (namely, I read the IRC logs now) and it's getting harder to pick up: I would certainly agree with a dyn-like syntax for the dynamic shim, although I also understand that calling it dyn might not be ideal.

Shameless plug and a small note just for the record: You can get "anonymous" sum types and products easily with:

@Centril Yeah, that stuff from frunk is super cool. However, note that in order for CoprodInjector::inject to work, the resulting type has to be inferrable, which is usually impossible without naming the resulting type (e.g. -> Coprod!(A, B, C)). It's often the case that you're working w/ unnameable types, so you'd need -> Coprod!(impl Trait, impl Trait, impl Trait), which will fail inference because it doesn't know which variant should contain which impl Trait type.

@cramertj Very true (sidenote: each "variant" may not be entirely unnameable, but only partially, e.g: Map<Namable, Unnameable>).

The enum impl Trait idea has been discussed before in https://internals.rust-lang.org/t/pre-rfc-anonymous-enums/5695

@Centril Yeah, that's true. I'm specifically thinking of futures, where I often write things like

fn foo(x: Foo) -> impl Future<Item = (), Error = Never> {
    match x {
        Foo::Bar => do_request().and_then(|res| ...).left().left(),
        Foo::Baz => do_other_thing().and_then(|res| ...).left().right(),
        Foo::Boo => do_third_thing().and_then(|res| ...).right(),
    }
}

@cramertj I wouldn't say anonymous enum is similar to enum impl Trait, because we cannot conclude X: Tr && Y: Tr(X|Y): Tr (counter example: Default trait). So library authors will need to manually impl Future for (X|Y|Z|...).

@kennytm Presumably we'd want to autogenerate some trait impls for anonymous enums, so it seems like basically the same feature.

@cramertj Since an anonymous enum can be named (heh), if a Default impl is generated for (i32|String), we would be able to write <(i32|String)>::default(). OTOH <enum impl Default>::default() simply won't compile, so no matter what we auto-generate, it would still be safe since it can't be invoked at all.

Nevertheless, there are some cases where auto-generation can still cause problem with enum impl Trait. Consider

pub trait Rng {
    fn next_u32(&mut self) -> u32;
    fn gen<T: Rand>(&mut self) -> T where Self: Sized;
    fn gen_iter<'a, T: Rand>(&'a mut self) -> Generator<'a, T, Self> where Self: Sized;
}

It is perfectly normal that, if we've got an mut rng: (XorShiftRng|IsaacRng) we could compute rng.next_u32() or rng.gen::<u64>(). However, rng.gen_iter::<u16>() cannot be constructed because auto-generation can only produce (Generator<'a, u16, XorShiftRng>|Generator<'a, u16, IsaacRng>), while what we actually want is Generator<'a, u16, (XorShiftRng|IsaacRng)>.

(Maybe the compiler can automatically reject a delegation-unsafe call just like the Sized check.)

FWIW this feature strikes me as being closer in spirit to closures than to tuples (which are, of course, the anonymous struct counterpart to the hypothetical anonymous enums). The ways in which these things are "anonymous" is different.

For anonymous structs and enums (tuples and "disjoins"), the "anonymous" is in the sense of "structural" (as opposed to "nominal") types -- they're built-in, fully generic over their component types, and aren't a named declaration in any source file. But the programmer still writes them out and uses them like any other type, trait implementations for them are written down explicitly as usual, and they aren't particularly magical (apart from having built-in syntax and being 'variadic', which other types can't yet be). In some sense, they have a name, but instead of being alphanumeric, their 'name' is the syntax used to write them (parentheses and commas).

Closures, on the other hand, are anonymous in the sense that their name is secret. The compiler generates a fresh type with a fresh name each time you write one, and there is no way to find out what that name is or refer to it even if you wanted to. The compiler implements a trait or two for this secret type, and the only way you can interact with it is through these traits.

Being able to return different types from different branches of an if, behind an impl Trait, seems closer to the latter -- the compiler implicitly generates a type to hold the different branches, implements the requested trait on it to dispatch to the appropriate one, and the programmer never writes down or sees what that type is, and can't refer to it nor has any real reason to want to.

(In fact, this feature feels kind of related to the hypothetical "object literals" -- which would be for other traits what the existing closure syntax is for Fn. That is, instead of a single lambda expression, you'd implement each method of the given trait (with self being implicit) using the variables in scope, and the compiler would generate an anonymous type to hold the upvars, and implement the given trait for it, it'd have an optional move mode in the same way, and so on. Anyway, I suspect a different way of expressing if foo() { (some future) } else { (other future) } would be object Future { fn poll() { if foo() { (some future).poll() } else { (other future).poll() } } } (well, you'd also need to lift the result of foo() out into a let so it'd only be run once). That's rather less ergonomic and probably shouldn't be thought of as an actual *alternative to the other feature, but it suggests there's a relationship. Maybe the former could desugar into the latter, or something.)

@glaebhoerl that's a very interesting idea! There's also some prior art from Java here.

Some thoughts off the top of my head (so not very baked):

  1. [bikeshed] the prefix object suggests that this is a trait object rather than just existential -- but it isn't.

A possible alternative syntax:

impl Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
// ^ --
// this conflicts with inherent impls for types, so you have to delay
// things until you know whether `Future` is a type or a trait.
// This might be __very__ problematic.

// and perhaps (but probably not...):
dyn Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
  1. [macros/sugar] you can provide some trivial syntactic sugar so that you get:
future!(if foo() { a.poll() } else { b.poll() })

Yeah the syntax question is a mess because it's not clear whether you want to draw inspiration from struct literals, closures, or impl blocks :) I just picked one off the top of my head for example's sake. (Anyway, my main point was not that we should go and add object literals [though we should] but that I think anonymous enums are a red herring here [though we should add them too].)

Being able to return different types from different branches of an if, behind an impl Trait, seems closer to the latter -- the compiler implicitly generates a type to hold the different branches, implements the requested trait on it to dispatch to the appropriate one, and the programmer never writes down or sees what that type is, and can't refer to it nor has any real reason to want to.

Hmm. So I had assumed that rather than generate "fresh names" for enum types, we would instead leverage | types, corresponding to impls like this:

impl<A: IntoIterator, B: IntoIterator> IntoIterator for (A|B)  { /* dispatch appropriately */ }

Obviously there would be coherence issues with this, in the sense that multiple functions will generate identical impls. But even leaving those aside, I realize now that this idea may not work for other reasons -- e.g., if there are multiple associated types, in some contexts they might have to be the same, but in others be allowed to be different. For example maybe we return:

-> impl IntoIterator<Item = Y>

but somewhere else we do

-> impl IntoIterator<IntoIter = X, Item = Y>

These would be two overlapping impls I guess that can't be "coallesced"; well, maybe with specialization.

Anyway, the notion of "secret enums" seems cleaner all around I suppose.

I would like to register one final concern with existential impl trait. A substantial fraction of the times that I want to use impl trait, I actually want to return more than one type.

@nikomatsakis: Is it fair to say that in this case what is being returned is closer to a dyn Trait than to impl Trait, because the synthetic/anonymous return value implements something akin to dynamic dispatch?

cc https://github.com/rust-lang/rust/issues/49288, an issue I've been hitting a lot lately working with Futures and Future-returning trait methods.

Since this is the last chance before FCP closes, I'd like to make one last argument against automatic auto traits. I realize this is a bit last minute, so at most I'd like to formally address this issue before we commit to the current implementation.

To clarify for anyone who hasn't been following impl Trait, this is the issue I'm presenting. A type represented by impl X types currently automatically implement auto traits if and only if the concrete type behind them implements said auto traits. Concretely, if the following code change is made, the function will continue to compile, but any usages of the function relying on the fact that the type it returns implements Send will fail.

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(simpler example: working, internal changes causes failure)

This issue is not clear cut. There was a very delibrate decision to have auto traits "leak": if we didn't, we'd have to put + !Send + !Sync on every function which returns something non-Send or non-Sync, and we'd have an unclear story with potential other custom auto-traits which could simply not be implementable on the concrete type the function is returning. These are two problems I will touch on later.

First, I'd like to simply state my objection to the issue: this allows changing a function body to change the public facing API. This directly reduces the maintainability of code.

Throughout the development of rust, decisions have been made which err on the side of verbosity over usability. When newcomers see these, they think it's verbosity for the sake of verbosity, but this is not the case. Each decision, whether it's to not have structures automatically implement Copy, or to have all types explicit at function signatures, is for the sake of maintainability.

When I introduce people to Rust, sure, I can show them speed, productivity, memory safety. But go has speed. Ada has memory safety. Python has productivity. What Rust has trumps all of these, it has maintainability. When a library author wants to change an algorithm to be more efficient, or when they want to redo the structure of a crate, they have a strong guarantee from the compiler that it will tell them when they make mistakes. In rust, I can be assured that my code will continue to function not just in terms of memory safety, but logic and interface as well. _Every function interface in Rust is fully representable by the function's type declaration_.

Stabilizing impl Trait as is has a large chance of going against this belief. Sure, it's extremely nice for the sake of writing code quickly, but if I want to prototype I'll use python. Rust is the language of choice when one needs long-term maintainability, not short-term write-only code.


I say there's only a "large chance" of this being bad here because again, the issue isn't clear cut. The whole idea of 'auto traits' in the first place is non-explicit. Send and Sync are implemented based on what the contents of a struct, not the public declaration. Since this decision worked out for rust, impl Trait acting similarly could also work out well.

However, functions and structures are used differently in a codebase, and these aren't the same issues.

When modifying the fields of a structure, even private fields, it's immediately clear one's changing the real contents of it. Structures with non-Send or non-Sync fields made that choice, and library maintainers know to double check when a PR changes a structure's fields.

When modifying the internals of a function, it's definitely clear one can affect both performance and correctness. However, in Rust, we don't need to check that we're returning the right type. Function declarations are a hard contract we must uphold, and rustc watches our back. It's a thin line between auto traits on structs and in function returns, but changing the internals of a function is much more routine. Once we have full generator-powered Futures, it will be even more routine to modify functions returning -> impl Future. These will all be changes authors need to screen for modified Send/Sync implementations if the compiler doesn't catch it.

To resolve this, we could decide that this is an acceptable maintenance burden, as the original RFC discussion did. This section in the conservative impl trait RFC lays out the biggest arguments for leaking auto traits ("OIBIT"s being the old name for auto traits).

I've already laid out my main response to this, but here's one last note. Changing a structure's layout is not that common to do; it can be guarded against. The maintenance burden in ensuring functions continue to implement the same auto traits is greater than that of structures simply because functions change a lot more.


As a final note, I'd like to say that automatic auto traits is not the only option. It's the option we chose, but the alternative of opt-out auto traits is still an alternative.

We could require functions returning non-Send / non-Sync items to either state + !Send + !Sync or to return a trait (alias possibly?) which has those bounds. This wouldn't be a good decision, but it might be better than the one we're currently choosing.

As for the concern regarding custom auto traits, I would argue that any new auto traits should be not-implemented only for new types introduced after the auto trait. This might provide more of an issue than I can address now, but it's not one we can't address with more design.


This is very late, and very long winded, and I'm certain I've raised these objections before. I'm glad just to be able to comment one last time, and ensure we're fully OK with the decision we're making.

Thank you for reading, and I hope that the final decision sets Rust in the best direction it can go in.

Expanding on @daboross's review wrt. trait aliases, one could improve ergonomics of non-leaking auto traits like so:

trait FutureNSS<T, E> = Future<Item = T, Error= E> + !Send + !Sync;

fn does_some_operation() -> impl FutureNSS<(), ()> {
     let data_stored = Rc::new("hello");
     some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

This is not so bad -- you'd have to come up with a good name (which FutureNSS is not). The main benefit is that it reduces the paper cut incurred by repetition of the bounds.

Wouldn't it be possible to stabilize this feature with the requirements to state auto-traits explicitely and later maybe remove those requirements once we found a suitable solution to that maintenance problem or once we are certain enough that there are in fact no maintenance burdens by the decision to lift the requirements?

What about requiring Send unless it is marked as !Send, but not providing Sync unless marked as Sync? Isn't Send supposed to be more common compared to Sync?

Like this:

fn provides_send_only1() -> impl Trait {  compatible_with_Send_and_Sync }
fn provides_send_only2() -> impl Trait {  compatible_with_Send_only }
fn fails_to_complile1() -> impl Trait {  not_compatible_with_Send }
fn provides_nothing1() -> !Send + impl Trait { compatible_with_Send}
fn provides_nothing2() -> !Send + impl Trait { not_compatible_with_Send }
fn provides_send_and_sync() -> Sync + impl Trait {  compatible_with_Send_and_Sync }
fn fails_to_compile2() -> Sync + impl Trait { compatible_with_Send_only }

Is there an inconsistency between impl Trait in argument position and return position wrt. auto traits?

fn foo(x: impl ImportantTrait) {
    // Can't use Send cause we have not required it...
}

This makes sense for argument position cause if you were allowed to assume Send here, you would get post monomorphization errors. Of course, the rules for return position and argument position are not required to coincide here, but it presents a problem in terms of learnability.

As for the concern regarding custom auto traits, I would argue that any new auto traits should be not-implemented only for new types introduced after the auto trait.

Well, this is true for the upcoming auto trait Unpin (only not-implmented for self-referential generators), but... that seems to be dumb luck? Is this a limitation we can really live with? I can't believe that there won't be something in the future that would need to be disabled for e.g. &mut or Rc...

I believe this has been discussed, and this is of course very late, but I'm still unsatisfied with impl Trait in argument position.

The abilities to both a) work with closures/futures by value, and b) treat some types as "outputs" and thus implementation details, are idiomatic and have been since before 1.0, because they directly support Rust's core values of performance, stability, and safety.

-> impl Trait is thus merely fulfilling a promise made by 1.0, or removing an edge case, or generalizing existing features: it adds output types to functions, taking the same mechanism that has always been used to handle anonymous types and applying it in more cases. It may have been more principled to start with abstract type, i.e. output types for modules, but given that Rust doesn't have an ML module system the order isn't a big deal.

fn f(t: impl Trait) instead feels like it was added "just because we can," making the language bigger and stranger without giving enough back in return. I've struggled and failed to find some existing framework to fit it into. I understand the argument around the conciseness of fn f(f: impl Fn(...) -> ...), and the justification that bounds can already be in both <T: Trait> and where clauses, but those feel hollow. They don't negate the downsides:

  • You now have to learn two syntaxes for bounds- at least <>/where share a single syntax.

    • This also creates a learning cliff and obscures the idea of using the same generic type in multiple places.

    • The new syntax makes it harder to tell what a function is generic over- you have to scan the whole argument list.

  • Now what should be a function's implementation detail (how it declares its type parameters) becomes part of its interface, because you can't write its type!

    • This also ties into the auto trait complications currently under discussion- further muddling of what is a function's public interface and what is not.

  • The analogy with dyn Trait is, honestly, a false one:

    • dyn Trait always means the same thing, and doesn't "infect" its surrounding declarations other than through the existing auto trait mechanism.

    • dyn Trait is usable in data structures, and this is really one of its primary use cases. impl Trait in data structures makes no sense without looking at all of the data structure's uses.

    • Part of what dyn Trait means is type erasure, but impl Trait doesn't imply anything about its implementation.

    • The previous point will be even more confusing if we introduce non-monomorphized generics. In fact, in such a situation, fn f(t: impl Trait) will likely a) not work with the new feature, and/or b) require even more edge case lawyering like the issue with auto traits. Imagine fn f<dyn T: Trait>(t: T, u: dyn impl Urait)! :scream:

So what it comes down to for me is that impl Trait in argument position adds edge cases, uses more of the strangeness budget, makes the language feel bigger, etc. while impl Trait in return position unifies, simplifies, and makes the language hang together more tightly.

What about requiring Send unless it is marked as !Send, but not providing Sync unless marked as Sync? Isn't Send supposed to be more common compared to Sync?

That feels very… arbitrary and ad-hoc. Maybe it's less typing, but more remembering and more chaos.

Bike shed-y idea here so as not to distract from my points above: instead of impl, use type? That's the keyword used for associated types, it's likely (one of) the keyword(s) used for abstract type, it's still fairly natural, and it hints more at the idea of "output types for functions":

// keeping the same basic structure, just replacing the keyword:
fn f() -> type Trait

// trying to lean further into the concept:
fn f() -> type R: Trait
fn f() -> type R where R: Trait
fn f() -> (i32, type R) where R: Trait
// or perhaps:
fn f() -> type R: Trait in R
// or maybe just:
fn f() -> type: Trait

Thank you for reading, and I hope that the final decision sets Rust in the best direction it can go in.

I appreciate the well authored objection. As you pointed out, auto traits have always been a deliberate choice to "expose" some implementation details that one might have expected to remain hidden. I think that -- thus far -- that choice has actually worked out quite well, but I confess that I am constantly nervous about it.

It seems to me that the important question is the extent to which functions really are different from structs:

Changing a structure's layout is not that common to do; it can be guarded against. The maintenance burden in ensuring functions continue to implement the same auto traits is greater than that of structures simply because functions change a lot more.

It's really hard to know how true this will be. It seems like the general rule is going to be that introducing Rc is something to be done with caution -- it's not so much a question of where you store it. (Actually, the case I really work about is not Rc but rather introducing dyn Trait, since that can be less obvious.)

I strongly suspect that in code that is returning futures, working with non-thread-safe types and so forth will be rare. You will tend to avoid those sorts of libraries. (Also, of course, it always pays to have tests exercising your code in realistic scenarios.)

In any case, this is frustrating because it is the sort of thing that is hard to know in advance, no matter how long of a stabilization period we give it.

As a final note, I'd like to say that automatic auto traits is not the only option. It's the option we chose, but the alternative of opt-out auto traits is still an alternative.

True, though I definitely feel nervous about the idea of "singling out" specific auto traits like Send. It's also keeping in mind that there are other use cases for impl trait besides futures. For example, returning iterators or closures -- and in those cases, it's not obvious that you would want send or sync by default. In any case, what you would really want, and what we are trying to defer =), is a kind of "conditional" bound (Send if T is Send). This is precisely what auto traits give you.

@rpjohnst

I believe this has been discussed

Indeed, it has :) ever since the first impl Trait RFC lo these many years ago. (Woah, 2014. I feel old.)

I've struggled and failed to find some existing framework to fit it into.

I feel quite the opposite. To me, without impl Trait in argument position, impl Trait in return position stands out all the more. The unifying thread that I see is:

  • impl Trait -- where it appears, it indicates that there will be "some monomorphized type that implements Trait". (The question of who specifies that type -- the caller or the callee -- depends on where the impl Trait appears.)
  • dyn Trait -- where it appears, it indicates that there will be some type that implements Trait, but that the choice of type is made dynamically.

There are also plans to expand the set of places where impl Trait can appear, building on that intuition. For example, https://github.com/rust-lang/rfcs/pull/2071 permits

let x: impl Trait = ...;

The same principle applies: the choice of type is known statically. Similarly, the same RFC introduces abstract type (for which impl Trait can be understood as a kind of syntacti sugar), which can appear in trait impls and even as members in modules.

Bike shed-y idea here so as not to distract from my points above: instead of impl, use type?

Personally, I'm not inclined to reinstigate a bikeshed here. We spent quite some time discussing syntax in https://github.com/rust-lang/rfcs/pull/2071 and elsewhere. There doesn't seem to be a "perfect keyword", but reading impl as "some type that implements" works pretty well imo.

Let me add a bit more about auto trait leakage:

First off, ultimately I think that auto trait leakage is actually the right thing to do here, precisely because it is consistent with the rest of the language. Auto traits were -- as I said earlier -- always a gamble, but they seem to have been one that has basically paid off. I just don't see impl Trait being so very different.

But also, I am pretty nervous about delaying here. I agree that there are other interesting points in the design space and I am not 100% confident we have hit the right spot, but I don't know that we will ever be sure of that. I am pretty worried if we delay now we will have a hard time delivering on our roadmap for the year.

Finally, let's consider the implications if I am wrong: What we are basically talking about here is that semver becomes even more subtle to judge. This is a concern, I think, but one that can be mitigated in various ways. For example, we can use lints that warn when !Send or !Sync types are introduced. We have long talked about introducing a semver checker that helps you to prevent accidental semver violations -- this seems like another case where that would help. In short, a problem, but not I think a critical one.

So -- at least as of this moment -- I still feel inclined to continue the current path.

Personally, I'm not inclined to reinstigate a bikeshed here.

I'm not terribly invested in it either; it was an afterthought based on my impression that impl Trait in argument position seem to be motivated by "filling in holes" syntactically rather than semantically, which seems to be correct given your response. :)

To me, without impl Trait in argument position, impl Trait in return position stands out all the more.

Given the analogy to associated types, this comes across very much like "without type T in argument position, associated types stand out all the more." I suspect that particular objection hasn't come up because the syntax we've chosen makes it feel nonsensical- the existing syntax there is good enough that nobody feels the need for syntactic sugar like trait Trait<type SomeAssociatedType>.

We already have syntax for "some monomorphized type that implements Trait." In the case of traits, we have both "caller" and "callee"-specified variants. In the case of functions, we only have the caller-specified variant, so we need the new syntax for the callee-specified variant.

Expanding this new syntax to local variables might be justified, because that is also very much an associated-type-like situation- it's a way to hide+name an expression's output type, and is useful for forwarding callee functions' output types.

Like I mentioned in my previous comment, I am also a fan of abstract type. It is, again, simply an expansion of the "output type" concept to modules. And applying -> impl Trait, let x: impl Trait, and abstract type's use of inference to trait impls' associated types is also great.

It's specifically the concept of adding this new syntax for function arguments that I dislike. It doesn't do the same thing as any of the other features it's being pulled in with. It does do the same thing as the syntax we already have, just with more edge cases and less applicability. :/

@nikomatsakis

It's really hard to know how true this will be.

It seems to me that we should err on the side of being conservative then? Can we gain more confidence in the design with more time (by letting auto trait leakage be under a separate feature gate and only in nightly while we stabilize the rest of impl Trait)? We can always add support for auto trait leakage later on if we don't leak now..

But also, I am pretty nervous about delaying here. [..] I am pretty worried if we delay now we will have a hard time delivering on our roadmap for the year.

Understandable! However, and as I'm sure you have considered, the decisions here will live with us for many years to come.

For example, we can use lints that warn when !Send or !Sync types are introduced. We have long talked about introducing a semver checker that helps you to prevent accidental semver violations -- this seems like another case where that would help. In short, a problem, but not I think a critical one.

This is good to hear! 🎉 And I think this mostly mollifies my concerns.

True, though I definitely feel nervous about the idea of "singling out" specific auto traits like Send.

I very much agree with this sentiment 👍.

In any case, what you would really want, and what we are trying to defer =), is a kind of "conditional" bound (Send if T is Send). This is precisely what auto traits give you.

I feel as tho T: Send => Foo<T>: Send would be better understood if the code explicitly stated this.

fn foo<T: Extra, trait Extra = Send>(x: T) -> impl Bar + Extra {..}

Tho, as we discussed in WG-Traits, you might not get inference at all here, so you always have to specify Extra if you want something other than Send, which would be a total bummer.

@rpjohnst

The analogy with dyn Trait is, honestly, a false one:

With respect to impl Trait in argument position it is false, but not so with -> impl Trait as both are existential types.

  • Now what should be a function's implementation detail (how it declares its type parameters) becomes part of its interface, because you can't write its type!

I'd like to note that the order of type parameters has never been an implementation detail due to turbofish, and in this respect, I think that impl Trait can help since it allows you to leave certain type arguments unspecified in turbofish.

[..] the existing syntax there is good enough that nobody feels the need for syntactic sugar like trait Trait.

Never say never? https://github.com/rust-lang/rfcs/issues/2274

Like @nikomatsakis, I really appreciate the care taken in these last-minute comments; I know that it can feel like trying to throw yourself in front of a freight train, especially for a feature as long desired as this one!


@daboross, I wanted to drill into the opt-out idea a bit more. At first blush it seems promising, because it would allow us to fully state the signature, but with defaults that make the common case concise.

Unfortunately, though, it runs into some problems once you start looking at the bigger picture:

  • If auto traits were treated as opt-out for impl Trait, they should be for dyn Trait as well.
  • This of course applies even when these constructs are used in argument position.
  • But then, it would be rather odd for generics to behave differently. In other words, for fn foo<T>(t: T), you could reasonably expect T: Send by default.
  • We of course have a mechanism for this, currently applied only to Sized; it's a trait that's assumed by default everywhere, and for which you opt out by writing ?Sized

The ?Sized mechanism remains one of the most obscure and hard to teach aspect of Rust, and we have in general been extremely loathe to expand it to other concepts. Using it for a concept as central as Send seems risky -- not to mention, of course, that it would be a big breaking change.

What's more, though: we really don't want to bake in an auto trait assumption for generics, because part of the beauty of generics today is that you can effectively be generic over whether a type implements an auto trait, and have that information just "flow through". For example, consider fn f<T>(t: T) -> Option<T>. We can pass in T regardless of whether it is Send, and the output will be Send iff T was. This is a hugely important part of the generics story in Rust.

There are also issues with dyn Trait. In particular, because of separate compilation, we'd have to restrict this "opt out" nature solely to "well known" auto traits like Send and Sync; it would probably mean never stabilizing auto trait for external use.

Finally, it's worth reiterating that the "leakage" design was explicitly modeled after what happens today when you create a newtype wrapper to return an opaque type. Fundamentally, I believe that "leakage" is an inherent aspect of auto traits in the first place; it has tradeoffs, but it's what the feature is at core, and I think we should strive for new features to interact with it accordingly.


@rpjohnst

I don't have a lot to add on the argument position issue after the extensive discussions on the RFC and @nikomatsakis's summary comment above.

Now what should be a function's implementation detail (how it declares its type parameters) becomes part of its interface, because you can't write its type!

I don't understand what you mean by this. Can you expand?

I also want to note that phrases like:

fn f(t: impl Trait) instead feels like it was added "just because we can"

undermine good faith discussion (I'm calling this out because it's a repeated pattern). The RFC goes to considerable lengths to motivate the feature and rebut some of the arguments you're making here -- not to mention the discussion on thread of course, and in previous iterations of the RFC, etc etc.

Tradeoffs exists, there are indeed downsides, but it doesn't help us reach a reasoned conclusion to caricature "the other side" of the debate.

Thanks to everyone for their detailed comments! I'm really super excited to finally ship impl Trait on stable, so I'm heavily biased towards the current implementation and the design decisions that led up to it. That said, I'll try my best to respond as impartially as possible and consider things as if we were starting from square zero:

auto Trait Leakage

The idea of auto Trait leakage bothered me for a long time-- in some ways, it can seem antithetical to many of Rust's design goals. Compared with its ancestors, such as C++ or the ML family, Rust is unusual in that it requires generic bounds to be explicitly stated in function declarations. In my opinion, this make Rust's generic functions easier to read and understand, and it makes it relatively clear when a backwards-incompatible change is being made. We've continued this pattern in our approach to const fn, requiring functions to explicitly specify themselves as const rather than inferring constness from function bodies. Much like explicit trait bounds, this makes it easier to tell which functions are usable in what ways, and gives library authors confidence that small implementation changes won't break users.

That said, I've used return-position impl Trait extensively in my own projects, including my work on the Fuchsia operating system, and I believe that auto trait leakage is the right default here. Practically, the consequence of removing leakage would be that I have to go back and add + Send to basically every impl Trait-using function I've ever written. Negative bounds (requiring + !Send) are an interesting idea to me, but then I'd be writing + !Unpin on almost all of the same functions. Explicitness is helpful when it informs users' decisions or makes code more understandable. In this case, I think it would do neither.

Send and Sync are "contexts" in which users progam: it is extremely rare that I write an application or library that uses both Send and !Send types (especially when writing async code to be run on a central executor, which is either multithreaded or not). The choice to be thread-safe or not is one of the first choices that has to be made when writing an application, and from there on, choosing to be thread-safe means that all my types must be Send. For libraries, it's nearly always the case that I prefer Send types, since not using them usually means that my library is unusable (or requires a creating a dedicated thread) when used within a threaded context. An uncontested parking_lot::Mutex will have nearly identical performance to RefCell when used on modern CPUs, so I don't see any motivation to push users towards specializing library functionality for !Send use-cases. For these reasons, I don't think it's important to be able to discern between Send and !Send types at the function-signature level, and I don't think that it will be commonplace for library authors to accidentally introduce !Send types into impl Trait types that were previously Send. It is true that this choice comes with a readability and clarity cost, but I believe the trade-off is well worth it for the ergonomic and usability benefits.

Argument-position impl Trait

I don't have too much to say here, except that every time I've reached for argument-position impl Trait, I've found that it greatly increased the readability and overall pleasantness of my function signatures. It's true that it doesn't add a novel capability that isn't possible in today's Rust, but it's a great quality-of-life improvement for complicated function signatures, it pairs well conceptually with return-position impl Trait, and it eases OOP programmers' transitions into happy Rustaceans. Currently, there's a lot of redundancy around having to introduce a named generic type just to provide a bound (e.g. F in fn foo<F>(x: F) where F: FnOnce() vs. fn foo(x: impl FnOnce())). This change solves that issue and results in functions signatures that are easier to read and write, and IMO feel like a natural fit alongside -> impl Trait.

TL; DR: I think our original decisions were the right ones, though they undoubtedly come with tradeoffs.
I really appreciate everyone speaking up and putting in so much time and effort to make sure that Rust is the best language it can be.

@Centril

With respect to impl Trait in argument position it is false, but not so with -> impl Trait as both are existential types.

Yes, that is what I meant.

@aturon

phrases like ... undermine good faith discussion

You're right, apologies for that. I believe I made my point better elsewhere.

Now what should be a function's implementation detail (how it declares its type parameters) becomes part of its interface, because you can't write its type!

I don't understand what you mean by this. Can you expand?

With support for impl Trait in argument position, you can write this function in two ways:

fn f(t: impl Trait)
fn f<T: Trait>(t: T)

The choice of form determines whether the API consumer can even write down the name of any particular instantiation (e.g. to take its address). The impl Trait variant doesn't let you do that, and this can't always be worked around without rewriting the signature to use the <T> syntax. Further, moving to the <T> syntax is a breaking change!

At the risk of further caricature, the motivation for this is that it is easier to teach, learn, and use. However, because the choice between the two is also a major part of the function's interface, just like type parameter order, I don't feel that this has been adequately addressed- I don't actually disagree that it's easier to use or that it results in more pleasant function signatures.

I'm not sure any of our other "simple, but limited -> complex, but general" changes, motivated by learnability/ergonomics, involve interface-breaking changes in quite this way. Either the simple way's complex equivalent behaves identically and you only need to switch when you're already changing the interface or behavior (e.g. lifetime elision, match ergonomics, -> impl Trait), or the change is just as general and intended to be applied universally (e.g. modules/paths, in-band lifetimes, dyn Trait).

To be more concrete, I worry we'll start hitting this problem in libraries, and it will be much like "everyone needs to remember to derive Copy/Clone," but worse because a) that will be a breaking change, and b) there will always be a tension to move back, specifically because that's what the feature was designed for!

@cramertj As far as the function signature redundancy goes... could we get rid of it some other way? In-band lifetimes were able to get away without backreferences; perhaps we could do the moral equivalent of "in-band type parameters" somehow. Or in other words, "the change is just as general and intended to be applied universally."

@rpjohnst

Further, moving to the <T> syntax is a breaking change!

Not necessarily, with https://github.com/rust-lang/rfcs/pull/2176 you could add an extra type parameter T: Trait to the end and turbofish would still work (unless you are referring to breakage by some other means than turbofish-breakage).

The impl Trait variant doesn't let you do that, and this can't always be worked around without rewriting the signature to use the <T> syntax. Further, moving to the <T> syntax is a breaking change!

Also, I think you mean that moving from the <T> syntax is a breaking change (because callers can no longer specify the value of T explicitly using turbofish).

UPDATE: Note that if a function uses impl Trait, then we currently don't permit using turbofish at all -- even if it has some normal generic parameters.

@nikomatsakis Moving to the explicit syntax can be a breaking change as well, if the old signature had a mixture of explicit type parameters and implicit ones -- anyone who provided n type parameters will now need to provide n + 1 instead. That was one of the cases @Centril's RFC aimed to solve.

UPDATE: Note that if a function uses impl Trait, then we currently don't permit using turbofish at all -- even if it has some normal generic parameters.

This technically reduces the number of breaking cases, but on the other hand it increases the number of cases where you can't name a specific instantiation. :(

@nikomatsakis

Thank you for addressing this concern sincerely.

I'm still hesitant to say auto trait leaking is _the right_ solution, but I agree that we can't really know what's best until after the fact.

I'd mainly considered the Futures use case, but that's hardly the only one. Without leaking Send/Sync from local types, there isn't really a good story for using impl Trait in many different contexts. Given this, and given additional auto traits, my suggestion isn't really viable.

I hadn't wanted to single out Sync and Send and _only_ assume them, since that's a bit arbitrary and only best for _one_ use case. However, the alternative of assuming all auto traits wouldn't be good either. + !Unpin + !... on every type does not sound like a viable solution.

If we had another five years of language design to come up with an effects system and other ideas I've no clue about now, we might be able to come up with something better. But for now, and for Rust, it seems like having 100% "auto" auto traits is the best path forwards.

@lfairy

Moving to the explicit syntax can be a breaking change as well, if the old signature had a mixture of explicit type parameters and implicit ones -- anyone who provided n type parameters will now need to provide n + 1 instead.

That is not presently allowed. If you use impl Trait, you don't get turbofish for any parameters (as I noted). This is not intended as a long-term solution though, more of a conservative step to dodge disagreement on how to proceed until we have time to come up with a rounded design. (And, as @rpjohnst noted, it has its own downsides.)

The design I would like to see is (a) yes to accept @centril's RFC or something like it and (b) to say that you can use turbofish for explicit parameters (but not impl Trait types). However, we didn't do that, in part because we were wondering if there might be a story that enabled migration from an explicit parameter to an impl trait.

@lfairy

That was one of the cases @Centril's RFC aimed to solve.

_[Trivia]_ Incidentally, it was actually @nikomatsakis who brought it to my attention that partial turbofish could ease the breaks between <T: Trait> and impl Trait ;) It was not a goal of the RFC at all from the start, but it was a nice surprise. 😄

Hopefully, once we gain more confidence around inference, defaults, named parameters, etc. we can have partial turbofish as well, Eventually™.

The final comment period is now complete.

If this is being shipped in 1.26 then https://github.com/rust-lang/rust/issues/49373 seems very important to me, Future and Iterator are two of the major use-cases and they are both very dependent on knowing the associated types.

Did a quick search in the issue tracker, and #47715 is an ICE that still needs to be fixed. Can we get this before it goes into stable?

Something I ran into with impl Trait today:
https://play.rust-lang.org/?gist=69bd9ca4d41105f655db5f01ff444496&version=stable

Looks like impl Trait is incompatible with unimplemented!() - is this a known issue?

yes, see #36375 and #44923

I have just realised that RFC 1951's assumption 2 runs up against some of my planned uses of impl Trait with async blocks. Specifically if you take a generic AsRef or Into parameter to have a more ergonomic API, then transform that into some owned type before returning an async block, you still get the returned impl Trait type being bound by any lifetimes in that parameter, e.g.

impl HttpClient {
    fn get(&mut self, url: impl Into<Url>) -> impl Future<Output = Response> + '_ {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

fn foo(client: &mut HttpClient) -> impl Future<Output = Response> + '_ {
    let url = Url::parse("http://foo.example.com").unwrap();
    client.get(&url)
}

With this you will get an error[E0597]: `url` does not live long enough because get includes the temporary reference's lifetime in the returned impl Future. This example is slightly contrived in that you could pass the url by value into get, but there will almost certainly be similar cases coming up in real code.

As far as I can tell the expected fix for this is abstract types, specifically

impl HttpClient {
    abstract type Get<'a>: impl Future<Output = Response> + 'a;
    fn get(&mut self, url: impl Into<Url>) -> Self::Get<'_> {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

By adding the layer of indirection you must explicitly pass through which generic type and lifetime parameters are required for the abstract type.

I'm wondering if there's potentially a more succinct way to write this, or will this just end up with abstract types being used for almost every function and never the bare impl Trait return type?

So, if I understand @cramertj's comment on that issue you would get an error on the definition of HttpClient::get something like `get` returns an `impl Future` type which is bounded to live for `'_`, but this type could potentially contain data with a shorter lifetime inside the type of `url`. (Because the RFC explicitly specifies that impl Trait captures _all_ generic type parameters, and it's a bug that you are allowed to capture a type that may contain a lifetime shorter than your explicitly declared lifetime).

From this the only fix still appears to be declaring a nominal abstract type to allow explicitly declaring which type parameters are captured.

Actually, that seems like it would be a breaking change. So if an error in that case is going to be added, it better be soon.

EDIT: And re-reading the comment, I don't think that's what it's saying, so I'm still confused on whether there's a potential way around this without using abstract types or not.

@Nemo157 Yes, fixing #42940 would fix your lifetime problem since you can specify that the return type should live as long as the borrow of self, irrespective of the lifetime of Url. This is definitely a change we want to make, but I believe it's backwards-compatible to do so-- it's not allowing the return type to have a shorter lifetime, it's over-restricting the ways in which the return type can be used.

For example, the following errors with "the parameter Iter may not live long enough":

fn foo<'a, Iter>(_: &'a mut u32, iter: Iter) -> impl Iterator<Item = u32> + 'a
    where Iter: Iterator<Item = u32>
{
    iter
}

Just having the Iter in the generics for the function is not enough to allow it to be present in the return type, but currently the callers of the function incorrectly assume that it is. This is definitely a bug and should be fixed, but I believe that it can be fixed backwards-compatibly and shouldn't block stabilization.

It appears #46541 is done. Can someone update the OP?

Is there a reason the syntax abstract type Foo = ...; was chosen over type Foo = impl ...;? I much preferred the latter, for consistency of syntax, and I recall some discussion over this a while ago, but can't seem to find it.

I'm partial to type Foo = impl ...; or type Foo: ...;, abstract seems an unnecessary oddball.

If I recall correctly, one of the main concerns was that people have learned to interpret type X = Y like a textual substitution ("replace X with Y where applicable"). This doesn't work for type X = impl Y.

I prefer type X = impl Y myself because my intuition is that type works like let, but...

@alexreg There's lots of discussion on the topic on RFC 2071. TL;DR: type Foo = impl Trait; breaks the ability to desugar impl Trait into some "more explicit" form, and it breaks people's intuitions about type aliases working as a slightly smarter syntactic substitution.

I'm partial to type Foo = impl ...; or type Foo: ...;, abstract seems an unnecessary oddball

You should join my exists type Foo: Trait; camp :wink:

@cramertj Hmm. I've just refreshed myself on some of that, and if I'm honest I can't say I understand @withoutboats's reasoning. It seems both the most intuitive to me (do you have a counter-example?) and the bit about desugaring I just don't get. I guess my intuition works like @lnicola. I also feel this syntax is the best for doing things like https://github.com/rust-lang/rfcs/pull/2071#issuecomment-319012123 – can this even be done in the current syntax?

exists type Foo: Trait; is a slightly improvement, though I would still drop the exists keyword. type Foo: Trait; wouldn't bother me enough to complain. 😉 abstract is just superfluous/oddball, as @eddyb says.

@alexreg

can this even be done in the current syntax?

Yes, but it's much more awkward. This was my primary reason for preferring the = impl Trait syntax (modulo the abstract keyword).

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item=impl Display>;

// can be written like this:

exists type Foo1: Bar;
exists type Foo2: Baz;
exists type Foo: (Foo1, Foo2);

exists type IterDisplayItem: Display;
exists type IterDisplay: Iterator<Item=IterDisplayItem>;

Edit: exists type Foo: (Foo1, Foo2); above should've been type Foo = (Foo1, Foo2);. Sorry for the confusion.

@cramertj The syntax seems nice. Should exists be a proper keyword?

@cramertj Right, I was thinking you'd have to do something like that... a good reason to prefer= impl Trait, I reckon! Honestly, if people think the intuition about substitution breaks down sufficiently for existential types here (compared to simple type aliases), then why not the following compromise?

exists type Foo = (impl Bar, impl Baz);

(Honestly though, I'd rather just have the consistency of using the single type keyword for everything.)

I find:

exists type Foo: (Foo1, Foo2);

deeply strange. Using Foo: (Foo1, Foo2) where the RHS is not a bound is not consistent with how Ty: Bound is used elsewhere in the language.

The following forms seem fine to me:

exists type Foo: Bar + Baz;  // <=> "There exists a type Foo which satisfies Bar and Baz."
                             // Reads super well!

type Foo = impl Bar + Baz;

type Bar = (impl Foo, impl Bar);

I also prefer to not use abstract as a word here.

I find exists type Foo: (Foo1, Foo2); deeply strange

That certainly looks like a mistake to me, and I think it should say type Foo = (Foo1, Foo2);.

If we're bikeshedding abstract type vs exists type here, I'd definitely support the former. Mostly because "abstract" works as an adjective. I could easily call something an "abstract type" in conversation, whereas it feels strange to say we're making an "exists type".

I'd also still prefer : Foo + Bar to : (Foo, Bar), = Foo + Bar, = impl Foo + Bar or = (impl Foo, impl Bar. The usage of + works well with all other places bounds can be, and the lack of = really signifies that we can't write out the full type. We aren't making a type alias here, we're making a name for something that we guarantee to have certain bounds, but that we can't name explicitly.


I also still like the syntax suggestion from https://github.com/rust-lang/rfcs/pull/2071#issuecomment-318852774 of:

type ExistentialFoo: Bar;
type Bar: Baz + Bax;

Though this is, as mentioned in that thread, a bit too little of a difference and not very explicit.

I must be interpreting (impl Foo, impl Bar) very differently from some of you... to me, this means the type is a 2-tuple of some existential types, and is completely different from impl Foo + Bar.

@alexreg If that was @cramertj's intent, I would still find that very strange with the : syntax:

exists type Foo: (Foo1, Foo2);

seems still very unclear as to what it's doing - bounds don't usually specify a tuple of possible types in any case, and it could easily be confused for the meaning of Foo: Foo1 + Foo2 syntax.

= (impl Foo, impl Bar) is an interesting idea - allowing creating existential tuples with types which aren't themselves known would be interesting. I don't think we _need_ to support those though, since we can just introduce two existential types for impl Foo and impl Bar then a third type alias for the tuple.

@daboross Well, you are making an "existential type", not an "exists type"; which is what it is called in type theory. But I think the phrasing "there exists a type Foo which ..." works nicely both with the mental model and from a type theoretical perspective.

I don't think we need to support those though, since we can just introduce two existential types for impl Foo and impl Bar then a third type alias for the tuple.

That seems unergonomic... temporaries are not so nice imo.

@alexreg Note: I didn't mean to say that impl Bar + Baz; is the same as (impl Foo, impl Bar), the latter is obviously the 2-tuple.

@daboross

If that was @cramertj's intent, I would still find that very strange with the : syntax:

exists type Foo: (Foo1, Foo2);

seems still very unclear as to what it's doing - bounds don't usually specify a tuple of possible types in any case, and it could easily be confused for the meaning of Foo: Foo1 + Foo2 syntax.

It's maybe a little unclear (not as explicit as (impl Foo, impl Bar), which I would intuitively understand right away) – but I don't think I'd ever confuse it for Foo1 + Foo2, personally.

= (impl Foo, impl Bar) is an interesting idea - allowing creating existential tuples with types which aren't themselves known would be interesting. I don't think we need to support those though, since we can just introduce two existential types for impl Foo and impl Bar then a third type alias for the tuple.

Yeah, that was an early proposal, and I still like it quite a lot. It's been noted that this can be done anyway using the current syntax, but it requires 3 lines of code, which is not very ergonomic. I also maintain that some syntax like ... = (impl Foo, impl Bar) is the clearest to the user, but I know there's contention here.

@Centril I didn't think so at first, but it was slightly ambiguous, and then @daboross seemed to interpret it that way, hah. Anyway, glad we've cleared that up.

Whoops, see my edit to https://github.com/rust-lang/rust/issues/34511#issuecomment-386763340. exists type Foo: (Foo1, Foo2); should've been type Foo = (Foo1, Foo2);.

@cramertj Ah, that makes more sense now. Anyway, don't you think being able to do the following is the most ergonomic? Even browsing that other thread, I haven't really seen a good argument against it.

type A = impl Foo;
type B = (impl Foo, impl Bar, String);

@alexreg Yes, I do think that is the most ergonomic syntax.

Using RFC https://github.com/rust-lang/rfcs/pull/2289, this is how I'd rewrite @cramertj's snippet:

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item: Display>;

// alternatively:

exists type IterDisplay: Iterator<Item: Display>;

type IterDisplay: Iterator<Item: Display>;

However, I think for type aliases, not introducing exists would help retain expressive power while not needlessly making the language's syntax more complex; so from a complexity budget POV, impl Iterator seems better than exists. The last alternative doesn't really introduce new syntax however and is also the shortest while being clear.

In summation, I think both of the following forms should be allowed (because it works under both the impl Trait and bounds on associated types syntaxes we already have):

type Foo = (impl Bar, impl Baz);
type IterDisplay: Iterator<Item: Display>;

EDIT: Which syntax should be used? IMO, clippy should unambiguously prefer the Type: Bound syntax when it's possible to use as it is most ergonomic and direct.

I much prefer the type Foo: Trait variant over the type Foo = impl Trait variant. It matches the associated type syntax, which is good because it's also an "output type" of the module that contains it.

The impl Trait syntax is used for both input and output types already, which means it risks giving the impression of polymorphic modules. :(

If impl Trait were solely used for output types, then I might prefer the type Foo = impl Trait variant on the grounds that the associated type syntax is more for traits (which correspond loosely to ML signatures) while the type Foo = .. syntax is more for concrete modules.

@rpjohnst

I much prefer the type Foo: Trait variant over the type Foo = impl Trait variant.

I agree, it should be used whenever possible; but what about the case of (impl T, impl U) where the bound syntax can't be used directly? It seems to me that introducing temporary type aliases hurts readability.

Using just bare type Name: Bound seems like it would be confusing when used inside impl blocks:

impl Iterator for Foo {
    type Item: Display;

    fn next(&mut self) -> Option<Self::Item> { Some(5) }
}

For both that syntax and the current(?) plan of keyword prefix the cost of introducing temporary type aliases to be used in impl blocks is also much larger, these type aliases now need to be exported at the module level (and given a semantically meaningful name...), which blocks a relatively common pattern (at least for me) of defining trait implementations inside private modules.

pub abstract type First: Display;
pub abstract type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

vs

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@Nemo157 Why not allow both:

pub type First: Display;
pub type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

and:

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

?

I don't see why there needs to be two syntaxes for the same feature, using just the type Name = impl Bound; syntax explicitly providing names for the two parts would still be possible:

pub type First = impl Display;
pub type Second = impl Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@Nemo157 I agree there don't need to (and shouldn't) be two different syntaxes. I don't find type (with no prefix keyword) confusing at all though, I must say.

@rpjohnst What the heck is a polymorphic module? :-) Anyway, I don't see why we should be modelling the syntax after associated type definitions, which are placing trait bounds on a type. This is nothing to do with bounds.

@alexreg A polymorphic module is one that has type parameters, the same way fn foo(x: impl Trait) does. It's not something that exists, and so I don't want people to think it does.

abstract type (edit: to name the feature, not to suggest the use of the keyword) has everything to do with bounds! Bounds are the only thing you know about the type. The only difference between them and associated types is that they're inferred, because they're usually unnameable.

@Nemo157 the Foo: Bar syntax is already more familiar in other contexts (bounds on associated types, and on type parameters) and is more ergonomic and (IMO) clear when it can be used without introducing temporaries.

Writing:

type IterDisplay: Iterator<Item: Display>;

seems a whole lot more direct wrt. what I want to say, compared to

type IterDisplay = impl Iterator<Item = impl Display>;

I think this is just consistently applying syntax we already have; so it is not really new.

EDIT2: The first syntax is also how I'd want it rendered in rustdoc.

Going from a trait that requires something on an associated type, to an impl also becomes really easy:

trait Foo {
    type Bar: Baz;
    // stuff...
}

struct Quux;

impl Foo for Quux {
    type Bar: Baz; // Oh look! Same as in the trait; I had to do nothing!
    // stuff...
}

The impl Bar syntax seems better when you'd otherwise have to introduce temporaries, but it is also applying syntax consistently throughout.

Being able to use both syntaxes wouldn't really be much different from being able to use impl Trait in argument position as well as having an explicit type parameter T: Trait that is then used by an argument.

EDIT1: In fact, having only one syntax would be special casing, not the other way around.

@rpjohnst I beg to differ, although I should have said it has nothing explicitly to do with bounds.

Anyway, I’m not against the type Foo: Bar; syntax, but for goodness sake, let’s get rid of the abstract keyword. type by itself is quite clear, in any circumstance.

Personally, I feel that using = and impl is a nice visual hint that inference is happening. It also makes it easier to spot those places when skimming a larger file.

Also, assuming I see type Iter: Iterator<Item = Foo> I'll have to find Foo and figure out if it's a type or a trait before I know what's going on.

And lastly, I think the visual clue of inference points will also help debugging inferrence errors and interpreting inference error messages.

So I do think the =/impl variant solves a bit more papercuts.

@phaylon

Also, assuming I see type Iter: Iterator<Item = Foo> I'll have to find Foo and figure out if it's a type or a trait before I know what's going on.

This I don't get; Item = Foo should always be a type nowadays given that dyn Foo is stable (and bare trait is getting phased out...)?

@Centril

This I don't get; Item = Foo should always be a type nowadays given that dyn Foo is stable (and bare trait is getting phased out...)?

Yeah, but in the proposed implless variant, it could be an inferred type with a bound, or a concrete type. E.g. Iterator<Item = String> vs Iterator<Item = Display>. I have to know the traits to know if inference is happening.

Edit: Ah, didn't notice one used :. Kinda what I mean with easy to miss :) But you're right that they're different.

Edit 2: I do think this problem would hold outside of associated types. Given type Foo: (Bar, Baz) you'd need to know Bar and Baz to know where inferrence happens.

@Centril

EDIT1: In fact, having only one syntax would be special casing, not the other way around.

There is currently only one way to declare _existential_ types, -> impl Trait. There are two ways to declare _universal_ types (T: Trait and : impl Trait in an argument list).

If we had polymorphic modules that took in universal types, I could see some arguments around that, but I believe the current usage of type Name = Type; in both modules and trait definitions is as an output type parameter, which should be an existential type.


@phaylon

Yeah, but in the proposed implless variant, it could be an inferred type with a bound, or a concrete type. E.g. Iterator<Item = String> vs Iterator<Item = Display>. I have to know the traits to know if inference is happening.

I believe the implless variant is using : Bound in all cases for existential types, so you could have Iterator<Item = String> or Iterator<Item: Display> as trait bounds, but Iterator<Item = Display> would be an invalid declaration.

@Nemo157
You're right with regard to the associated type case, my bad there. But (as noted in my edit) I think there's still an issue with type Foo: (A, B). As either A or B could be a type or trait.

I believe this is also a good reason to go with =. The : only tells you that some things are inferred, but doesn't tell you which. type Foo = (A, impl B) seems clearer to me.

I also assume reading and providing code snippets is easier with impl, as additional context about what is a trait and what isn't never needs to be provided.

Edit: Some Credits: My argument is basically the same as @alexreg's here, I just wanted to expand on why I think impl is preferrable.

There is currently only one way to declare existential types, -> impl Trait. There are two ways to declare universal types (T: Trait and : impl Trait in an argument list).

That's what I'm saying :P I Why should universal quantification have two ways but existential only one (ignoring dyn Trait) in other places?

It seems equally probable to me that a user would go and write type Foo: Bound; and type Foo = impl Bound; having learned different parts of the language, and I can't say that one syntax is distinctly better in all cases; It is clear to me that one syntax is better for some things and another for different things.

@phaylon

I believe this is also a good reason to go with =. The : only tells you that some things are inferred, but doesn't tell you which. type Foo = (A, impl B) seems clearer to me.

Yes, this is probably another good reason. It really takes some unpacking to figure out what's being existentially quantified over – jumping from definition to definition.

Another thing is: would one even allow : within an associated type binding, under that syntax? It would seem like a bit of an odd special case to me, given that existential types can't be composed/combined in any other way in this proposed syntax. I would imagine the following would be the most consistent approach using that syntax:

type A: Foo;
type B: Bar;
type C: Baz;
type D: Iterator<Item = C>; 
type E = (A, Vec<B>, D);

Using the syntax that I (and some others here) prefer, we could write this all in a single line, and furthermore it's immediately clear where the quantification is happening!

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item = impl Baz>);

Unrelated to the above: when do we play to implement let x: impl Trait in nightly? I've been missing this feature for a while now.

@alexreg

Another thing is: would one even allow : within an associated type binding, under that syntax?

Yes, why not; This would be a natural effect of rust-lang/rfcs#2289 + type Foo: Bound.

You could also do:

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item: Baz>);

@Centril I think it's a bad idea to allow two alternative syntaxes. Smells like "we couldn't decide, so we'll just support both" syndrome. Seeing code that mix & matches them will be a real eyesore!

@Centril I'm kind of with @nikomatsakis on that RFC of yours BTW, sorry. Would rather write impl Iterator<Item = impl Baz>. Nice and explicit.

@alexreg This is fair;

But (un)fortunately (depending on your POV), we already started the "allow two alternative syntaxes" with impl Trait in argument position, such that we have both Foo: Bar and impl Bar working to mean the same thing;

It is for universal quantification, but the impl Trait notation doesn't really care about what side of the duality it is on; after all, we didn't go with any Trait and some Trait.

Given that we already made the choice "we couldn't decide" and "the side of the duality doesn't matter syntactically", it seems to me consistent to apply "we can't decide" everywhere so that user's don't get into "but I could write it like this over there, why not here?" ;)


PS:

Re. impl Iterator<Item = impl Baz> it doesn't work as a bound in a where clause; so you'd have to mix it like Iter: Iterator<Item = impl Baz>. You'd have to allow: Iter = impl Iterator<Item = impl Baz> for it to work uniformly (maybe we should?).

Using : Bound is also instead of = impl Bound is also explicit, just shorter ^,-
I think the difference in spacing between X = Ty and X: Ty makes the syntax legible.

Having ignored my own advice, let's continue this conversation at the RFC ;)

But (un)fortunately (depending on your POV), we already started the "allow two alternative syntaxes" with impl Trait in argument position, such that we have both Foo: Bar and impl Bar working to mean the same thing;

We did, but I believe the choice was made more from a standpoint of symmetry/consistency. Generically-typed arguments are strictly more powerful than universally-typed (impl Trait) ones. But we were introducing impl Trait in the return position, it made sense to introduce it the argument position.

Given that we already made the choice "we couldn't decide" and "the side of the duality doesn't matter syntactically", it seems to me consistent to apply "we can't decide" everywhere so that user's don't get into "but I could write it like this over there, why not here?" ;)

I'm not sure it's at the point we should throw our arms up and say "let's implement everything". There's not as clear an argument here as to the gain.

PS:

Re. impl Iterator<Item = impl Baz> it doesn't work as a bound in a where clause; so you'd have to mix it like Iter: Iterator<Item = impl Baz>. You'd have to allow: Iter = impl Iterator<Item = impl Baz> for it to work uniformly (maybe we should?).

I'd say we either just support where Iter: Iterator<Item = T>, T: Baz (like we have now) or go the whole way with Iter = impl Iterator<Item = impl Baz> (as you suggested). Only allowing the half-way house seems a bit of a cop-out.

Using : Bound is also instead of = impl Bound is also explicit, just shorter ^,-
I think the difference in spacing between X = Ty and X: Ty makes the syntax legible.

It's legible, but I don't think it's nearly as clear/explicit that an existential type is being used. This is exacerbated when the definition has to be split across multiple lines due to the limitation of this syntax.

Having ignored my own advice, let's continue this conversation at the RFC ;)

Wait, you mean your RFC? I think it's relevant to both that and this one, from what I can tell. :-)

Wait, you mean your RFC? I think it's relevant to both that and this one, from what I can tell. :-)

OK; Let's continue here then;

We did, but I believe the choice was made more from a standpoint of symmetry/consistency. Generically-typed arguments are strictly more powerful than universally-typed (impl Trait) ones. But we were introducing impl Trait in the return position, it made sense to introduce it the argument position.

My whole point is about consistency and symmetry. =P
If you are allowed to write impl Trait both for existential and universal quantification, to me it makes sense that you should also be allowed to use Type: Trait for both universal and existential quantification.

Regarding expressive power, the former is more powerful than the latter as you say, but this doesn't necessarily have to be the case; They could be equally powerful if we wanted them to be AFAIK (tho I'm absolutely not saying we should do that..).

fn foo(bar: impl Trait, baz: typeof bar) { // eww... but possible!
    ...
}

I'm not sure it's at the point we should throw our arms up and say "let's implement everything". There's not as clear an argument here as to the gain.

My argument is that surprising users with "This syntax is usable elsewhere and it's meaning is clear here, but you can't write it in this place" costs more than having two ways to do it (which you both have to familiarize yourself with anyways). We've done similar things with https://github.com/rust-lang/rfcs/pull/2300 (merged), https://github.com/rust-lang/rfcs/pull/2302 (PFCP), https://github.com/rust-lang/rfcs/pull/2175 (merged) where we fill consistency holes even tho it was possible to write in another way before.

It's legible, but I don't think it's nearly as clear/explicit that an existential type is being used.

Legible is sufficient in my view; I don't think Rust ascribes to "explicit above all" and being overly verbose (which I do find the syntax to be when used too much) also costs by discouraging use.
(If you want something to be used often, give it terser syntax... c.f ? as a bribe against .unwrap()).

This is exacerbated when the definition has to be split across multiple lines due to the limitation of this syntax.

This I don't get; It seems to me that Assoc = impl Trait should cause line splits even more so than Assoc: Trait simply because the former is longer.

I'd say we either just support where Iter: Iterator<Item = T>, T: Baz (like we have now) or go the whole way with Iter = impl Iterator<Item = impl Baz> (as you suggested).
Only allowing the half-way house seems a bit of a cop-out.

Exactly!, let's not go half-way house / cop-out and implement where Iter: Iterator<Item: Baz> ;)

@Centril Okay, you've won me over, principally on the symmetry/consistency argument. 😉 The ergonomics of having both forms helps too though. Heavy linting is a must for this feature though, before long.

Will edit this with my full reply tomorrow.

Edit

As @Centril points out, we already support universal types using the : Trait (bound) syntax. e.g.

fn foo<T: Trait>(x: T) { ... }

alongside "proper" or "reified" universal types, e.g.

fn foo(x: impl Trait) { ... }

Of course, the former is more powerful than the latter, but the latter is more explicit (and arguably more legible) when it is all that is required. In fact, I strongly believe we should have a compiler lint in favour of the latter form where possible.

Now, we already have impl Trait in the function return position too, which represents an existential type. The associated types of traits are existential in form, and already use the : Trait syntax.

This, given the existence of what I shall both the proper and bound forms of universal types in Rust at present, and likewise the existence of proper and bound forms for existential types (the latter only within traits at present), I strongly believe we should extend support for both the proper and bound forms of existential types to outside of traits. That is, we should support the following both generally and for associated types.

type A: Iterator<Item: Foo + Bar>;
type B = (impl Baz, impl Debug, String);

I second the compiler linting behaviour suggested in this comment too, which should strongly reduce the variation of expression of common existential types in the wild.

I still believe that conflating universal and exxisistential quantification under one keyword was a mistake, and so that consistency argument doesn’t work for me. The only reason a single keyword works in function signatures is the context necessarily constrains you to only use one form of quantification in each position. There are potential sugars I could see being a thing where you don’t have the same constraints

struct Foo {
    pub foo: impl Display,
}

Is this shorthand for existential or universal quantification? From the intuition derived from using impl Trait in function signatures I don’t see how you could decide. If you actually attempt to use it as both you will quickly realise that anonymous universal quantification in this position is useless, so it must be existential quantification, but that apperas inconsistent with impl Trait in function arguments.

These are two fundamentally different operations, yes they both use trait bounds, but I don’t see any reason that having two ways to declare an existential type would reduce confusion for newcomers. If attempting to use type Name: Trait is actually a likely thing for newcomers to do then this could be solved via a lint:

    type Foo: Display;
    ^^^^^^^^^^^^^^^^^^
note: were you attempting to create an existential type?
note: suggested replacement `type Foo = impl Display`

And I just came up with an alternative formulation of your argument that I would be much more amenable to, it will have to wait till I’m at a real computer to reread the RFC and post about though.

I feel like I don't have enough experience with Rust yet to be commenting on RFCs. However, I am interested in seeing this feature merged into nightly and stable Rust, in order to use it with Rust libp2p for building a sharding protocol for Ethereum as part of the Drops of Diamond sharding implementation. I subscribed to the issue, however I don't have time to keep up with all the comments! How can I stay up to date on a high level on this issue, without having to skim read comments?

I feel like I don't have enough experience with Rust yet to be commenting on RFCs. However, I am interested in seeing this feature merged into nightly and stable Rust, in order to use it with Rust libp2p for building a sharding protocol for Ethereum as part of the Drops of Diamond sharding implementation. I subscribed to the issue, however I don't have time to keep up with all the comments! How can I stay up to date on a high level on this issue, without having to skim read comments? At the moment it looks like I may just have to do that by checking in from time to time, and not subscribing to the issue. It would be good if I could subscribe by email to get high-level news on this.

I still believe that conflating universal and existential quantification under one keyword was a mistake, and so that consistency argument doesn’t work for me.

As a general principle, independent of this feature, I find this line of reasoning problematic.

I believe that we should approach language design from how a language is rather than how we wished it were under some alternate unfolding of history. The syntax impl Trait as universal quantification in argument position is stabilized wherefore you can't wish it away. Even if you believe X, Y and Z were mistakes (and I could find plenty of things I personally think are mistakes in Rust's design, but I accept and assume them...), we have to live with them now, and I think about how we can make everything fit together given the new feature (make things consistent).

In discussion, I think the entire corpus of RFCs and the language as-is should be taken as if not axioms, then strong arguments.


You could make the case (but I would not) that:

struct Foo {
    pub foo: impl Display,
}

is semantically equivalent to:

struct Foo<T: Display> {
    pub foo: T,
}

under the function-argument reasoning.

Basically, given impl Trait, you have to think "is this return-type-like, or argument like?", which can be difficult.


If attempting to use type Name: Trait is actually a likely thing for newcomers to do then this could be solved via a lint:

I would also lint, but in the other direction; I think that the following ways should be idiomatic:

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

Ok, alternative formulation that I believe RFC 2071 is hinting at and may have been discussed in the issue, but was never explicitly stated:

There is only _one way_ to declare existentially quantified types: existential type Name: Bound; (using existential because that's specified in the RFC, I'm not entirely against dropping the keyword under this formulation).

There is additionally sugar for implicitly declaring an unnamed existentially quantified type at the current scope: impl Bound (ignoring the universal quantification sugar in function arguments for the time being).

So, the current return type usage is a simple desugaring:

fn foo() -> impl Iterator<Item = impl Display> { ... }
existential type _0: Display;
existential type _1: Iterator<Item = _0>;
fn foo() -> _1 { ... }

extending to const, static and let is similarly trivial.

The one extension not mentioned in the RFC is: supporting this sugar in type Alias = Concrete; syntax, so when you write

type Foo = impl Iterator<Item = impl Display>;

this is actually sugar for

existential type _0: Display;
existential type _1: Iterator<Item = _0>;
type Foo = _1;

which then relies on the transparent nature of type aliases to allow the current module to look through Foo and see that it refers to an existential type.

In fact, I strongly believe we should have a compiler lint in favour of the latter form where possible.

I'm mostly aligned with @alexreg's comment, but I have some concerns about linting towards arg: impl Trait, mainly due to the risk of encouraging semver breaking changes in libraries since impl Trait doesn't work with turbofish (right now, and you'd need partial turbofish to make it work nicely). Therefore, linting in clippy feels less straightforward than in the case of type aliases (where there's no turbofish to cause any problems).

I'm mostly aligned with @alexreg's comment, but I have some concerns about linting towards arg: impl Trait, mainly due to the risk of encouraging semver breaking changes in libraries since impl Trait doesn't work with turbofish (right now, and you'd need partial turbofish to make it work nicely). Therefore, linting in clippy feels less straightforward than in the case of type aliases (where there's no turbofish to cause any problems).

@Centril just brought this up on IRC with me, and I concur it's a fair point regarding backwards-compatibility (too easy to break). When/if partial turbofish lands, I think a compiler lint should be added, but not until then.

So... we've had quite a lot of discussion on the syntax for named existential types now. Shall we try to come to a conclusion and write it into the RFC / PR post, so someone can start work on the actual implementation? :-)

Personally, once we have named existentials, I would prefer a lint (if any) away from any use of impl Trait anywhere.

@rpjohnst Well you certainly agree with me and @Centril with regards to named existentials... as for linting away from them in function arguments, that's a possibility, but probably a discussion for another place. It depends whether one wishes to favour simplicity or generality in this context.

Is the RFC on impl Trait in argument position up to date? If so, is it safe to state that its semantics are _universal_? If so: I wanna cry. Deeply.

@phaazon: Rust 1.26 release notes for impl Trait:

Side note for you type theorists out there: this isn’t an existential, still a universal. In other words, impl Trait is universal in an input position, but existential in an output position.

Just to express my thoughts about that:

  • We already had a syntax for type variables and really, there’re actually a few uses for anynomous type variables (i.e. it’s very often that you want to use the type variable in several places instead of just dropping it at a single place).
  • Covariant existentials would open us the doors to rank-n functions, something that is hard to do right now without a trait (see this) and is a feature that is really missing to Rust.
  • impl Trait is easy to refer as “type picked by the callee”, because… because it’s the only language construct for now that enables us to do so! Picking the type by the caller is already available via several constructs.

I really think the impl Trait in argument position current decision is a pity. :cry:

I really think the impl Trait in argument position current decision is a pity. 😢

While I am a bit torn over this, I certainly think time would be better spent on implementing let x: impl Trait right now!

Covariant existentials would open us the doors to rank-n functions

We have already a syntax for it (fn foo(f: impl for<T: Trait> Fn(T))), (aka "type HRTB"), but it's not implemented yet. fn foo(f: impl Fn(impl Trait)) produces an error that "nested impl Trait is not allowed", and I expect we'll want it to mean the higher-rank version, when we get type HRTB.

This is similar to how Fn(&'_ T) means for<'a> Fn(&'a T), so I don't expect it to be controversial.

Looking at the current draft, impl Trait in argument position is a _universal_, but you’re saying that impl for<_> Trait turns it into an _existential_?! How crazy is that?

Why do we thought we had a need to introduce _yet another way_ to construct a _universal_? I mean:

fn foo(x: impl MyTrait)

Is only interesting because the anonymous type variable appears only once in the type. If you need to return the same type:

fn foo(x: impl Trait) -> impl Trait

Will obviously not work. We’re telling people to shift away from a more general idiom to a more restrictive one instead of just learning one and use it everywhere. That’s my whole rant. Adding a feature that has no value added – the learning assumption I read in the RFC is a strange argument where we think instead of the newcomers – they’ll still need to learn things, so why do we make the syntax more ambiguous to pretty much everyone in order to lower the learning curve here? When people will get used to universal vs. existential (and come one, the principle is very simple with the right words), people will start to think why we have the same keyword / patterns to express both and also where and in-place-template-parameters.

Argh I guess all of this was already accepted and I’m ranting for nothing. I just think it’s a real pity. I’m pretty sure I’m not the only one disappointed with the RFC decision.

(There is probably not much point continuing to debate this after the feature has been stabilized, but see here for a cogent argument (which I agree with) for why impl Trait in argument position having the semantics it does is sensible and coherent. Tl;dr it's for the same reason why fn foo(arg: Box<Trait>) works roughly the same way as fn foo<T: Trait>(arg: Box<T>), even though dyn Trait is an existential; now substitute dyn with impl.)

Looking at the current draft, impl Trait in argument position is a universal, but you’re saying that impl for<_> Trait turns it into an existential?!

No, they're both universal. I'm saying that higher-ranked uses would look like this:

fn foo<F: for<G: Fn(X) -> Y> Fn(G) -> Z>(f: F) {...}

which could, at the same time as it's added (i.e. with no changes to impl Trait) be written as:

fn foo(f: impl for<G: Fn(X) -> Y> Fn(G) -> Z) {...}

That's universal impl Trait, just that the Trait is a HRTB (similar to impl for<'a> Fn(&'a T)).
If we decide (which I expect is likely) that impl Trait inside Fn(...) arguments is also universal, you could write this to achieve the same effect:

fn foo(f: impl Fn(impl Fn(X) -> Y) -> Z) {...}

This is what I thought you meant by "higher-ranked", if you didn't, please let me know.

An even more interesting decision could be to apply the same treatment in existential position, i.e. allowing this (which would mean "return some closure that takes any other closure"):

fn foo() -> impl for<G: Fn(X) -> Y> Fn(G) -> Z {...}

to be written like this:

fn foo() -> impl Fn(impl Fn(X) -> Y) -> Z {...}

That would be an existential impl Trait containing an universal impl Trait (bound to the existential, instead of the enclosing function).

@eddyb Wouldn't it make more sense to have two separate keywords for existential and universal quantification in general, for consistency and to not confuse newbies?
Wouldn't the keyword for existential quantification also be re-useable for existential types?
Why are we using impl for existential (and universal) quantification but existential for existential types?

I'd like to make three points:

  • There's not much merit discussing if impl Trait is existential or universal. Most of the programmers out there probably didn't read enough type theory handbooks. The question should be if people like it or if they find it confusing. To answer that question, some form of feedback can be seen both here in this thread, on reddit or the forum. If something needs further explaining, it fails a litmust test for intuitive or non-surprising feature. So we should be looking at how many people and how confused they are and if it's more questions than with other feature. It is indeed sad this feedback arrives after stabilization and something should be done about this phenomenon, but it's for a separate discussion.
  • Technically, even after stabilization, there would be a way to get rid of the feature in this case (letting aside the decision if should). It would be possible to lint against writing functions which use that and remove the ability in the next edition (while preserving the ability to call them if they come from different-edition crates). That would satisfy rust stability guarantees.
  • No, adding two more keywords to specify existential and universal types would not improve on the confusion, it would only make things even worse.

It is indeed sad this feedback arrives after stabilization and something should be done about this phenomenon

There's been objections to impl Trait in the argument position as long as it's been an idea. Feedback like this _isn't new_, it was highly debated even in the relevant RFC thread. There was a lot of discussion not just about universal/existential types from a type-theory perspective, but also for how this would be confusing for new users.

True, we didn't get actual new users' perspectives, but this didn't come out of nowhere.

@Boscop any and some were proposed as a pair of keywords to do this job but were decided against (although I don't know if the rationale was ever written down anywhere).

True, we couldn't get feedback from people new to rust and who weren't type theorists

And the argument for inclusion was always that it would make it easier for newcomers. So if we now have actual feedback from newcomers, shouldn't that be very relevant kind of feedback instead of arguing how newcomers should understand it?

I guess if anyone had the time, some kind of research through the forums and other places how confused people were before and after the inclusion could be done (I wasn't very good at statistics, but I'm pretty sure someone who was could come up with something that's better than blind predictions).

And the argument for inclusion was always that it would make it easier for newcomers. So if we now have actual feedback from newcomers, shouldn't that be very relevant kind of feedback instead of arguing how newcomers should understand it?

Yes? I mean I'm not arguing whether what happened was a good or bad idea. I just mean to point out that the RFC thread did get feedback on this, and it was decided anyways.

As you said it is probably better to have the meta discussion on feedback somewhere else, though I'm not sure where that would be.

No, adding two more keywords to specify existential and universal types would not improve on the confusion, it would only make things even worse.

Worse? How is that so? I prefer having more to rembember than ambiguity / confusion.

Yes? I mean I'm not arguing whether what happened was a good or bad idea. I just mean to point out that the RFC thread did get feedback on this, and it was decided anyways.

Sure. But both arguing sides were old, scarred and experienced programmers with deep understanding of what happens under the hood, guessing about a group they are not part of (newcomers) and guessing about future. From a factual point of view, that's not much better than rolling dice in regards what actually happens in reality. This is not about inadequate experience of the experts, but about not having adequate data to base the decisions on.

Now it was introduced and we have a way to get the actual hard data, or as hard data as any can be gotten in land of how much people get confused on a scale from 0 to 10.

As you said it is probably better to have the meta discussion on feedback somewhere else

For example here, I've already started such discussion and there are some actual steps that can be taken, even if small ones: https://internals.rust-lang.org/t/idea-mandate-n-independent-uses-before-stabilizing-a-feature/7522/14. I didn't have the time to write the RFC, so if someone beats me to that or wants to help, I won't mind.

Worse? How is that so?

Because, unless impl Trait is deprecated, you have all 3, therefore having more to remember in addition to the confusion. If impl Trait was to go away, the situation would be different and it would be weighting of pros and cons of the two approaches.

impl Trait as in callee-picking would be sufficient. If you try to use it in argument position, then you introduce the confusion. The HRTBs would remove that confusion.

@vorner Previously I argued that we should do actual A/B testing with Rust newbies to see what they find actually easier and harder to learn, because it's hard to guess as someone who is proficient with Rust.
FWIW, I remember, when I was learning Rust (coming from C++, D, Java etc), the universally quantifying type generics (incl. their syntax) were easy to understand (lifetimes in generics were a little harder).
I think impl Trait for arg types will result in a lot of confusion from newbies down the line and many questions like this.
In the absence of any evidence for which changes would make Rust easier to learn, we should refrain from making such changes, and instead make changes that make/keep Rust more consistent because consistency makes it at least easy to remember. Rust newbies will have to read the book a couple times anyway, so introducing impl Trait for args to allow postponing generics in the book until later doesn't really take away any complexity.

@eddyb Btw, why do we need another existential keyword for types in addition to impl? (I wish we would use some for both..)

FWIW, I remember, when I was learning Rust (coming from C++, D, Java etc), the universally quantifying type generics (incl. their syntax) were easy to understand (lifetimes in generics were a little harder).

I myself also don't think it is a problem. In my current company, I'm leading Rust classes ‒ for now we meet once a week and I try to teach in practical implementation. The people are seasoned programmers, coming mostly from Java and Scala background. While there were some road blocks, generics (at least reading them ‒ they are a bit careful about actually writing them) in the argument position was a non-issue. There was a bit of a surprise about generics in return position (eg. the caller chooses what the function returns), especially that it can often be elided, but the explaining took like 2 minutes before it clicked. But I'm afraid to even mention the existence of impl Trait in argument position, because now I'd have to answer the question why it exists ‒ and I have no real answer to that. That is not good for motivation and having motivation is crucial for the learning process.

So, the question is, does the community have enough voice to re-open the debate with some data to back up the arguments?

@eddyb Btw, why do we need another existential keyword for types in addition to impl? (I wish we would use some for both..)

Why not forall… /me slowly sneaks away

@phaazon We have forall (i.e. "universal") and it's for, e.g. in HRTB (for<'a> Trait<'a>).

@eddyb Yep, then use it for existential as well, like Haskell does with forall, for instance.

The whole discussion is very opinionated, I’m a bit surprised that argument idea was stabilized looking. I hope there’s a way to push another RFC later to undo that (I’m completely willing to write it because I really really really don’t like all the confusion this is going to bring around).

I don't really get it. What's the point of having them in argument position? I don't write that much Rust, but I really liked being able to do -> impl Trait. When would I ever use it in the argument position?

My understanding was that it was mostly for consistency. If I can write the type impl Trait in an argument position in a fn signature, why can't I write it elsewhere?

That said, I would personally rather have just told people "just use a type parameters"...

Yes, it's for consistency. But I'm not sure it's a good enough argument, when type parameters are so easy to use. Also, the problem then arises of which to lint for/against!

Also, the problem then arises of which to lint for/against!

Considering you can't express several things with impl Trait at all, function with impl Trait as one of the arguments can't do turbofish and therefore you can't take its address (did I forget some other disadvantage?), I think it makes little sense to lint against type parameters, because you need to use them anyway.

therefore you can't take its address

You can, by having it inferred from the signature.

What's the point of having them in argument position?

There’s none because it’s the exact same thing as using a trait bound.

fn foo(x: impl Debug)

Is the exact same thing as

fn foo<A>(x: A) where A: Debug
fn foo<A: Debug>(x: A)

Also, consider this:

fn foo<A>(x: A) -> A where A: Debug

impl Trait in argument position doesn’t allow you to do this because it’s anonymized. This is then a pretty useless feature because we already have everything we need to deal with such situations. People won’t learn that new feature easily because pretty much everyone knows type variables / template parameters and Rust is the single language using that impl Trait syntax. That’s why a lot of people are ranting that it should have stayed to the return value / let bindings, because it introduced a new, needed semantics (i.e. type picked by the callee).

For short, @iopq: you won’t need this, and there’s no point other than “Let’s add another syntactic sugar construct no one will actually need because it copes with a very specific use – i.e. anonymized type variables”.

Also, something that I forgot to say: it makes it way harder to see how your function is parametered / monomorphized.

@Verner With partial turbofish, it makes a lot of sense to lint against it for the sake of simplicity, readability, explicitness. I’m not really for the feature in arg position to begin with though.

How is it consistent when -> impl Trait the callee choses the type, while in x: impl Trait the caller chooses the type?

I understand that there is no other way for it to work, but that doesn't seem "consistent", it seems the opposite of consistent

I truly agree that it’s everything but consistent and that people will be confused, newcomers as well as advanced proficient rustaceans.

We had two RFCs, which received a total of nearly 600 comments between them beginning more than 2 years ago, to resolve the questions being relitigated on this thread:

  • rust-lang/rfcs#1522 ("Minimal impl Trait")
  • rust-lang/rfcs#1951 ("Finalize syntax and parameter scoping for impl Trait, while expanding it to arguments")

(If you read these discussions, you'll see that I was initially a strong proponent of the two keywords approach. I now think using a single keyword is the right approach.)

After 2 years and hundreds of comments, a decision was reached and the feature has now been stabilized. This is the tracking issue for the feature, which is open to track the still unstable use cases for impl Trait. Relitigating the settled aspects of impl Trait is off topic for this tracking issue. You are welcome to continue talking about this, but please not on the issue tracker.

How has it been stabilised when impl Trait hasn’t even gotten support in argument position for fns in traits??

@daboross Then the checkbox in the original post needs to be ticked!

(Just discovering that https://play.rust-lang.org/?gist=47b1c3a3bf61f33d4acb3634e5a68388&version=stable currently works)

I think it's weird that https://play.rust-lang.org/?gist=c29e80715ac161c6dc95f96a7f91aa8c&version=stable&mode=debug doesn't work (yet), what's more with this error message. Am I the only one to think this way? Maybe it'd need adding a checkbox for impl Trait in return position in traits, or was it a conscious decision to only allow impl Trait in argument-position for trait functions, forcing use of existential type for return types? (which… would look inconsistent to me, but maybe I'm missing a point?)

@Ekleog

was it a conscious decision to only allow impl Trait in argument-position for trait functions, forcing use of existential type for return types?

Yes-- return position impl Trait in traits was put off until we have more practical experience using existential types in traits.

@cramertj Are we yet at the point where we have sufficient practical experience to implement that?

I would like to see impl Trait in a few stable releases before we add more features.

@mark-i-m I fail to see what's remotely controversial about return-position impl Trait for trait methods, personally... maybe I'm missing something.

I don't think it's controversial. I just feel like we're adding features too quickly. It would be nice to stop and focus on technical debt for a while and get experience with the current feature set first.

I see. I guess I just consider it a missing part of an existing feature more than a new feature.

I think @alexreg is right, it’s very tempting to use existential impl Trait on traits’ methods. It’s not really a new feature but I guess there’re a few things to address before trying to implement it?

@phaazon Perhaps, yes... I don't really know how much the implementation details would differ compared to what we already have today, but maybe someone could comment on that. I'd love to see existential types for let/const bindings too, but I can definitely accept that as a feature beyond this one, thus waiting another cycle or so before starting with it.

I wonder if we can hold back on universal impl Trait in traits...

But yeah, I guess I see your point.

@mark-i-m No we can't, they are already stable.

They are in functions, but what about Trait declarations?

@mark-i-m as the snippet shows, they are stable in both impls and trait declarations..

Just jumping in to catch up on where we're settling with abstract type. Personally, I'm pretty much on-board with @Centril's recently proposed syntax and best practices:

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

Which applied to some code of mine I guess would look something like:

// Concrete type with a generic body
struct Data<TBody> {
    ts: Timestamp,
    body: TBody,
}


// A name for an inferred iterator
type IterData = Data<impl Read>;
type Iter: Iterator<Item = IterData>;


// A function that gives us an iterator. Also takes some arbitrary range
fn iter(&self, range: impl RangeBounds<Timestamp>) -> Result<Iter, Error> { ... }


// A struct that holds on to that iterator
struct HoldsIter {
    iter: Iter,
}

It doesn't make sense to me that type Bar = (impl Display,); would be good, but type Bar = impl Display; would be bad.

If we're deciding on different alternative existential type syntaxes (all different from rfc 2071?), would a forum thread on https://users.rust-lang.org/ be a good place to do that?

I don't have enough of a grasp of the alternatives to start such a thread now, but as existential types are still unimplemented, I think discussion on the forums and then a new RFC would probably be better than talking about it in the tracking issue.

What’s wrong with type Foo = impl Trait?

@daboross Probably the internals forum instead. I'm considering writing an RFC about it tho to finalize the syntax.

@daboross There’s been more than enough discussion on the syntax on this thread already. I think if @Centril can write up an RFC for it at this point, then great.

Is there any issue I can subscribe to for discussion about existentials in traits?

Is there any macro-related argument for one syntax or the other?

@tomaka in the first case the type Foo = (impl Display,) is really the only syntax you've got. My preference for type Foo: Trait over type Foo = impl Trait just comes from the fact that we're binding a type we can name, like <TFoo: Trait> or where TFoo: Trait, whereas with impl Trait we can't name the type.

To clarify, I am not saying that type Foo = impl Bar is bad, I am saying type Foo: Bar is better in simple cases, in part due to @KodrAus's motivation.

The latter I read as: "the type Foo satisfies Bar" and the former as: "the type Foo is equal to some type which satisfies Bar". The former is thus, in my view, more direct and natural from an extensional view ("what I can do with Foo"). To understand the latter, you need to involve a deeper understanding of existential quantification of types.

type Foo: Bar is also quite neat because if that is the syntax used as the bound on an associated type in a trait, then you can simply copy the declaration in the trait to the impl and it will simply work (if that is all the information you want to expose..).

The syntax is also terser, especially when associated type bounds are involved and when there are many associated types. This can reduce the noise and therefore aid readability.

@KodrAus

Here's how I read those type definitions:

  • type Foo: Trait means "Foo is a type implementing Trait"
  • type Foo = impl Trait means "Foo is an alias of some type implementing Trait"

To me, Foo: Trait simply declares a constraint on Foo implementing Trait. In a way, type Foo: Trait feels incomplete. It looks like we have a constraint, but the actual definition of Foo is missing.

On the other hand, impl Trait is evocative of "this is a single type, but the compiler figures out its name". Therefore, type Foo = impl Trait implies we already have a concrete type (which implements Trait), of which Foo is just an alias.

I believe type Foo = impl Trait conveys the correct meaning more clearly: Foo is an alias of some type implementing Trait.

@stjepang

type Foo: Trait means "Foo is a type implementing Trait"
[..]
In a way, type Foo: Trait feels incomplete.

This is how I read it as well (modulo phrasing...), and it is an extensionally correct interpretation. This says all about what you can do with Foo (the morphisms that the type affords). Therefore, it is extensionally complete. From a readers and particularly a beginners perspective, I think extensionality is more important.

On the other hand, impl Trait is evocative of "this is a single type, but the compiler fills the gap". Therefore, type Foo = impl Trait implies we already have a concrete type (which implements Trait), of which Foo is an alias, but the compiler will figure out which type it really is.

This is a more detailed and intensional interpretation concerned with representation which is redundant from an extensional point of view. But this is more complete in an intensional sense.

@Centril

From a readers and particularly a beginners perspective, I think extensionality is more important.

This is a more detailed and intensional interpretation concerned with representation which is redundant from an extensional point of view

The extensional vs intensional dichotomy is interesting - I've never thought of impl Trait this way before.

Still, I beg to differ on the conclusion. FWIW, I've never managed to grok existential types in Haskell and Scala, so count me as a beginner. :) impl Trait in Rust has felt very intuitive from day one, which is probably due to the fact that I think of it as a restricted alias rather than what can be done with the type. So between knowing what Foo is and what can be done with it, I pick the former.

Just my 2c, though. Others might have different mental models of impl Trait.

I agree entirely with this comment: type Foo: Trait feels incomplete. And type Foo = impl Trait feels more analogous to uses of impl Trait elsewhere, which helps the language feel more consistent and memorable.

@joshtriplett See https://github.com/rust-lang/rust/issues/34511#issuecomment-387238653 for a start on the consistency discussion; I believe allowing form forms is in fact the consistent thing to do. And only allowing one of the forms (whichever...) is inconsistent. Allowing type Foo: Trait also fits particularly nicely with https://github.com/rust-lang/rfcs/pull/2289 with which you could state: type Foo: Iterator<Item: Display>; which makes things neatly uniform.

@stjepang The extensional perspective of type Foo: Bar; does not require you to understand existential quantification in type theory. All you really need to understand is that Foo allows you to do all the operations afforded by Bar, that's it. From a user of Foo's perspective, that's also all that is interesting.

@Centril

I believe I understand now where you're coming from and the appeal of pushing the Type: Trait syntax into as many places as possible.

There's a strong connotation around : being used for type-implements-trait bounds and = being used for type definitions and type-equals-another-type bounds.

I think this is apparent in your RFC as well. For example, take these two type bounds:

  • Foo: Iterator<Item: Bar>
  • Foo: Iterator<Item = impl Bar>

These two bounds in the end have the same effect, but are (I think) subtly different. The former says "Item must implement trait Bar", while the latter says "Item must be equal to some type implementing Bar".

Let me try illustrating this idea using another example:

trait Person {
    type Name: Into<String>; // Just a type bound, not a definition!
    // ...
}

struct Alice;

impl Person for Alice {
    type Name = impl Into<String>; // A concrete type definition.
    // ...
}

How should we define an existential type that implements Person then?

  • type Someone: Person, which looks like a type bound.
  • type Someone = impl Person, which looks like a type definition.

@stjepang Looking like a type bound is not a bad thing :) We can implement Person for Alice like so:

struct Alice;
trait Person          { type Name: Into<String>; ... }
impl Person for Alice { type Name: Into<String>; ... }

Look m'a! The stuff inside { .. } for both the trait and impl is identical, that means you can copy the text from the trait untouched as far as Name is concerned.

As an associated type is a type level function (where the first argument is Self), we can see a type alias as a 0-arity associated type, so nothing strange is happening.

These two bounds in the end have the same effect, but are (I think) subtly different. The former says "Item must implement trait Bar", while the latter says "Item must be equal to some type implementing Bar".

Yep; I find the first phrasing more to the point and natural. :)

@Centril Heh. Does that mean type Thing; alone is sufficient to introduce an abstract type?

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { type Output; fn neg(self) -> Self::Output { self } }

@kennytm I think it is technically possible; but you could ask if it is desirable or not depending on ones thoughts on implicit/explicit. In that particular case I think it would be technically sufficient to write:

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { fn neg(self) -> Self::Output { self } }

and the compiler could just infer type Output: Sized; for you (which is a deeply uninteresting bound that gives you no information). It is something to consider for more interesting bounds, but it won't be in my initial proposal because I think it might encourage low affordance APIs, even when the concrete type is very simple, due to programmer laziness :) Neither will type Output; be initially for the same reason.

I think after reading this all, I tend to agree more with @Centril. When I see type Foo = impl Bar I tend to think Foo is a particular type, like with other aliases. But it is not. Consider this example:

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

IMHO it's a bit weird seeing = in the declaration of Displayable but then not having the return types of foo and bar be equal (i.e. this = is not transitive, unlike everywhere else). The problem is that Foo is _not_ an alias for a particular type that happens to impl some Trait. Put another way, it is a single type in whatever context it is used but that type may be different for different uses, as in the example.

A few people mentioned that type Foo: Bar feels "incomplete". To me this is a good thing. In some sense Foo is incomplete; we don't know what it is, but we know it satisfies Bar.

@mark-i-m

The problem is that Foo is not an alias for a particular type that happens to impl some Trait. Put another way, it is a single type in whatever context it is used but that type may be different for different uses, as in the example.

Wow, is that really true? That'd certainly be very confusing to me.

Is there a reason why Displayable would be a shorthand for impl Display rather than a single concrete type? Is such behavior even useful considering that trait aliases (tracking issue: https://github.com/rust-lang/rust/issues/41517) can be used in a similar manner? Example:

trait Displayable = Display;

fn foo() -> impl Displayable { "hi" }
fn bar() -> impl Displayable { 42 }

@mark-i-m

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

That is not a valid example. From the reference section on existential types in RFC 2071:

existential type Foo = impl Debug;

Foo can be used as i32 in multiple places throughout the module. However, each function that uses Foo as i32 must independently place constraints upon Foo such that it must be i32

Each existential type declaration must be constrained by at least one function body or const/static initializer. A body or initializer must either fully constrain or place no constraints upon a given existential type.

Not directly mentioned, but required for the rest of the RFC to work, is that two functions in the scope of the existential type cannot determine a different concrete type for that existential. That will be some form of conflicting type error.

I would guess your example would give something like expected type `&'static str` but found type `i32` on the return of bar, since foo would have already set the concrete type of Displayable to &'static str.

EDIT: Unless you're coming to this from the intuition that

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

is equivalent to

fn foo() -> impl Display { "hi" }
fn bar() -> impl Display { 42 }

rather than my expectation of

existential type _0 = impl Display;
type Displayable = _0;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

I guess which of those two interpretations is correct might depend on the RFC that @Centril may be writing.

The problem is that Foo is not an alias for a particular type that happens to impl some Trait.

I guess which of those two interpretations is correct might depend on the RFC that @Centril may be writing.

The reason why type Displayable = impl Display; exists is that it is an alias for a particular type.
See https://github.com/rust-lang/rfcs/issues/1738, which is the problem that this feature solve.

@Nemo157 Your expectation is correct. :)

The following:

type Foo = (impl Bar, impl Bar);
type Baz = impl Bar;

would be desugared to:

/* existential */ type _0: Bar;
/* existential */ type _1: Bar;
type Foo = (_0, _1);

/* existential */ type _2: Bar;
type Baz = _2;

where _0, _1 and _2 are all nominally different types wherefore Id<_0, _1>, Id<_0, _2>, Id<_1, _2> (and the symmetric instances) are all uninhabited, where Id is defined in refl.

Disclaimer: I've (willingly) not read the RFC (but know what it's about), so that I could comment on what feels “intuitive” with syntaxes.

For the type Foo: Trait syntax, I would completely expect something like this to be possible:

trait Trait {
    type Foo: Display;
    type Foo: Debug;
}

In the same manner as where Foo: Display, Foo: Debug is currently possible.

If it is not allowed syntax, I think that's an issue with the syntax.

Oh, and I think the more syntax Rust has, the harder it becomes to learn it. Even if one syntax is “easier to learn”, so long as the two syntaxes are necessary the beginner will eventually have to learn both, and likely sooner rather than later if they get in for an already-existing project.

@Ekleog

For the type Foo: Trait syntax, I would completely expect something like this to be possible:

It is possible. Those "type aliases" declare associated types (type aliases can be interpreted as 0-ary type level functions while associated types are 1+-ary type level functions). Of course you can't have multiple associated types with the same name in one trait, that would be like trying to define two type aliases with the same name in a module. In an impl, type Foo: Bar also corresponds to existential quantification.

Oh, and I think the more syntax Rust has, the harder it becomes to learn it.

Both syntaxes are already used. type Foo: Bar; is already legal in traits, and also for universal quantification as Foo: Bar where Foo is a type variable. impl Trait is used for existential quantification in return position and for universal quantification in argument position. Allowing both plugs consistency gaps in the language. They are also optimal for different scenarios, and so having both give you the global optimum.

Furthermore, the beginner is unlikely to need type Foo = (impl Bar, impl Baz);. Most uses will likely be type Foo: Bar;.

The original pull request for RFC 2071 mentions a typeof keyword which seems to have been dismissed entirely in this discussion. I find the currently proposed syntax to be rather implicit, having both the compiler and any human who reads the code search for the concrete type.

I would prefer if this would be made explicit. So instead of

type Foo = impl SomeTrait;
fn foo_func() -> Foo { ... }

we would write

fn foo_func() -> impl SomeTrait { ... }
type Foo = return_type_of(foo_func);

(with the name of the return_type_of to be bikeshedded), or even

fn foo_func() -> impl SomeTrait as Foo { ... }

which would not even need new keywords and is easily understandable by anyone who knos impl Trait syntax. The latter syntax is concise and has all the information in one place. For traits it could look like this:

trait Bar
{
    type Assoc: SomeTrait;
    fn func() -> Assoc;
}

impl Bar for SomeType
{
    type Assoc = return_type_of(Self::func);
    fn func() -> Assoc { ... }
}

or even

impl Bar for SomeType
{
    fn func() -> impl SomeTrait as Self::Assoc { ... }
}

I am sorry if this has been discussed already and dismissed, but I was unable to find it.

@Centril

It is possible. Those "type aliases" declare associated types (type aliases can be interpreted as 0-ary type level functions while associated types are 1+-ary type level functions). Of course you can't have multiple associated types with the same name in one trait, that would be like trying to define two type aliases with the same name in a module. In an impl, type Foo: Bar also corresponds to existential quantification.

(sorry, I meant to put it in a impl Trait for Struct, not in a trait Trait)

I'm sorry, I'm not sure I understand. What I try to say, is, to me code like

impl Trait for Struct {
    type Type: Debug;
    type Type: Display;

    fn foo() -> Self::Type { 42 }
}

(playground link for full version)
feels like it should work.

Because it is just putting two bounds on Type, in the same way as where Type: Debug, Type: Display work.

If this is not to be allowed (what I seem to understand by “Of course you can't have multiple associated types with the same name in one trait”? but given my error in writing trait Trait instead of impl Trait for Struct I'm not sure), then I think that's an issue with the type Type: Trait syntax.

Then, inside a trait declaration, the syntax is already type Type: Trait, and doesn't allow multiple definitions. So I guess maybe this boat has already sailed long ago…

However, as pointed above by @stjepang and @joshtriplett, type Type: Trait feels incomplete. And while it may make sense in trait declarations (it's actually designed to be incomplete, even though it's weird it doesn't allow multiple definitions), it doesn't make sense in an impl Trait block, where the type is supposed to be known for sure (and currently can only be written as type Type = RealType)

impl Trait is used for existential quantification in return position and for universal quantification in argument position.

Yes, I also thought about impl Trait in argument position when writing this, and wondered whether I should say I'd have supported the same argument for impl Trait in argument position had I known it was undergoing stabilization. That said I think it'd be better not to re-ignite this debate :)

Allowing both plugs consistency gaps in the language. They are also optimal for different scenarios, and so having both give you the global optimum.

Optimum and simplicity

Well, I think sometimes losing the optimum in favor of simplicity is a good thing. Like, C and ML were born around the same time. C made huge concessions to the optimum in favor of simplicity, ML was much closer to the optimum but way more complex. Even counting derivatives of these languages, I don't think the number of C developers and of ML developers is comparable.

impl Trait and :

Currently, around the impl Trait and : syntaxes, I feel like there is a trend towards making both two alternative syntaxes for the same feature set. However, I don't think that's a good thing, as having two syntaxes for the same features can only confuse users, especially when they will always subtly differ in their exact semantics.

Imagine a beginner who always saw type Type: Trait coming upon their first type Type = impl Trait. They can likely guess what's happening, but I'm pretty sure there will be a moment of “WTF is that? I've been using Rust for years and there is still syntax I never saw?”. Which is more or less the trap in which C++ fell.

Feature bloat

What I'm thinking is, basically, the more features it has the harder the language is to learn. And I don't see a huge advantage of using type Type: Trait over type Type = impl Trait: it's, like, 6 characters saved?

Having rustc output an error when seeing type Type: Trait that says the person writing it to use type Type = impl Trait would make much more sense to me: at least there is a single way of writing things, it makes sense to all (impl Trait is already clearly recognized as an existential in return position), and it covers all use cases. And if people try to use what they think is intuitive (although I'd disagree with that, to me = impl Trait is more intuitive, compared to the current = i32), they get rightfully redirected to the conventionally-correct way of writing it.

The original pull request for RFC 2071 mentions a typeof keyword which seems to have been dismissed entirely in this discussion. I find the currently proposed syntax to be rather implicit, having both the compiler and any human who reads the code search for the concrete type.

typeof was briefly discussed in the issue I opened 1.5 years ago: https://github.com/rust-lang/rfcs/issues/1738#issuecomment-258353755

Speaking as a beginner, I find the type Foo: Bar syntax confusing. It's the associated type syntax, but those are supposed to be in traits, not structs. If you see impl Trait once, you can figure out what that is, or otherwise you can look it up. It's harder to do that with the other syntax, and I'm not sure what the benefit is.

It feels like some people on the language team are really opposed to using impl Trait to name existential types, so they'd rather use anything else instead. Even the comment here makes little sense to me.

But anyway, I think this horse was beaten to death. There are probably hundreds of comments about the syntax, and only a handful of suggestions (I realize I'm only making things worse). It's clear that no syntax won't make everyone happy, and there are arguments for and against all of them. Maybe we should just pick one and stick with it.

Woah, that's not at all what I understood. Thanks @Nemo157 for setting me straight!

In that case, I would indeed prefer the = syntax.

@Ekleog

then I think that's an issue with the type Type: Trait syntax.

It could be allowed and it would be perfectly well defined, but you typically write where Type: Foo + Bar instead of where Type: Foo, Type: Bar, so that doesn't seem like a very good idea. You could also easily fire a good error message for this case suggesting that you write Foo + Bar instead in the case of the associated type.

type Foo = impl Bar; also has understandability issues in that you see = impl Bar and conclude that you can just substitute it at each occurrence where it is used as -> impl Bar; but that would not work. @mark-i-m made this interpretation, which seems like a much more likely mistake to make. Therefore, I conclude that type Foo: Bar; is the better choice for learnability.

However, as pointed above by @stjepang and @joshtriplett, type Type: Trait feels incomplete.

It is not incomplete from an extensional POV. You get precisely as much information from type Foo: Bar; as you get from type Foo = impl Bar;. So from the perspective of what you can do with type Foo: Bar;, it is complete. In fact, the latter is desugared as type _0: Bar; type Foo = _0;.

EDIT: what I meant was that while it may feel incomplete for some, it isn't from a technical standpoint.

That said I think it'd be better not to re-ignite this debate :)

That's a good idea. We should consider the language as it is when designing, not as we wished it was.

Well, I think sometimes losing the optimum in favor of simplicity is a good thing.

If we should go for simplicity, I would instead drop type Foo = impl Bar; instead.
It should be noted that C's supposed simplicity (supposed, because Haskell Core and similar things are probably simpler while still sound..) comes at a steep price when it comes to expressivity and soundness. C is not my north star in language design; far from it.

Currently, around the impl Trait and : syntaxes, I feel like there is a trend towards making both two alternative syntaxes for the same feature set. However, I don't think that's a good thing, as having two syntaxes for the same features can only confuse users, especially when they will always subtly differ in their exact semantics.

But they will not differ whatsoever in their semantics. One desugars to the other.
I think the confusion of trying to write type Foo: Bar; or type Foo = impl Bar only for one of them not to work even tho both have perfectly well defined semantics is only in the way of the user. If a user tries to write type Foo = impl Bar;, then a lint fires and proposes type Foo: Bar;. The lint is teaching the user about the other syntax.
To me, it is important that the language is uniform and consistent; If we have decided to use both syntaxes somewhere, we should apply that decision consistently.

Imagine a beginner who always saw type Type: Trait coming upon their first type Type = impl Trait.

In this specific case, a lint would fire, and recommend the former syntax. When it comes to type Foo = (impl Bar, impl Baz);, the beginner will have to learn -> impl Trait in any case, so they should be able to infer the meaning from that.

Which is more or less the trap in which C++ fell.

C++s problem is mainly that it is quite old, has C's baggage, and a lot of features supporting too many paradigms. These are not distinct features, just different syntax.

What I'm thinking is, basically, the more features it has the harder the language is to learn.

I think learning a new language is mainly about learning its important libraries. That is where most time will be spent. The right features can make libraries much more composable and work in more cases. I much prefer a language that gives good abstractive power than one which forces you to think low-level and which causes duplication. In this case, we are not adding more abstractive power or not even really even features, just better ergonomics.

And I don't see a huge advantage of using type Type: Trait over type Type = impl Trait: it's, like, 6 characters saved?

Yes, only 6 characters saved. But if we consider type Foo: Iterator<Item: Iterator<Item: Display>>;, then we would instead get: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>>; which has a lot more noise. type Foo: Bar; also is more direct compared to the latter, less prone to misinterpretation (re. substitution..), and works better for associated types (copy the type from the trait..).
Further, type Foo: Bar could be naturally extended to type Foo: Bar = ConcreteType; which would expose the concrete type but also ensure that it satisfies Bar. No such thing can be done for type Foo = impl Trait;.

Having rustc output an error when seeing type Type: Trait that says the person writing it to use type Type = impl Trait would make much more sense to me: at least there is a single way of writing things,

they get rightfully redirected to the conventionally-correct way of writing it.

I'm proposing that there be one conventional way to write things; type Foo: Bar;.

@lnicola

Speaking as a beginner, I find the type Foo: Bar syntax confusing. It's the associated type syntax, but those are supposed to be in traits, not structs.

I'll reiterate that type aliases really can be seen as associated types. You will be able to say:

trait Foo        { type Baz: Quux; }
// User of `Bar::Baz` can conclude `Quux` but nothing more!
impl Foo for Bar { type Baz: Quux; }

// User of `Wibble` can conclude `Quux` but nothing more!
type Wibble: Quux;

We see that it works precisely the same in associated types and type aliases.

Yes, only 6 characters saved. But if we consider type Foo: Iterator<Item: Iterator<Item: Display>>;, then we would instead get: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>>; which has a lot more noise.

This seems orthogonal to the syntax for declaring a named existential. The four syntaxes I remember being proposed would all potentially allow this as

type Foo: Iterator<Item: Iterator<Item: Display>>;
type Foo = impl Iterator<Item: Iterator<Item: Display>>;
existential type Foo: Iterator<Item: Iterator<Item: Display>>;
existential type Foo = impl Iterator<Item: Iterator<Item: Display>>;

Being able to use your proposed shorthand Trait<AssociatedType: Bound> instead of Trait<AssociatedType = impl Bound> syntax for declaring anonymous existential types for the associated types of an existential type (either named or anonymous) is an independent feature (but probably relevant in terms of keeping the entire set of existential type features consistent).

@Nemo157 They are different features, yes; but I think it is natural to consider them together for the sake of consistency.

@Centril

I am sorry, but they are wrong. It is not incomplete from an extensional POV.

I never suggested that your proposed syntax was missing information; I was suggesting that it feels incomplete; it looks wrong to me, and to others. I understand that you disagree with that, from the perspective you're coming from, but that doesn't make that feeling wrong.

Also observe that in this thread, people demonstrated an interpretation problem with this exact syntax difference. type Foo = impl Trait feels like it makes it clearer that Foo is a specific but unnamed concrete type no matter how many times you use it, rather than an alias for a trait that can take on a different concrete type each time you use it.

I think it helps to tell people that they can take all the things they know about -> impl Trait and apply them to type Foo = impl Trait; there's a generalized concept impl Trait that they can see used as a building block in both places. Syntax like type Foo: Trait hides that generalized building block.

@joshtriplett

I was suggesting that it feels incomplete; it looks wrong to me, and to others.

Alright; I propose that we use a different term than incomplete here because to me, it suggests a lack of information.

Also observe that in this thread, people demonstrated an interpretation problem with this exact syntax difference.

What I observed was an interpretation mistake, made in the thread, about what type Foo = impl Bar; means. One person interpreted different uses of Foo as not being nominally the same type, but rather different types. That is, exactly: "an alias for a trait that can take on a different concrete type each time you use it".

Some have stated that type Foo: Bar; is confusing, but I'm not sure what the alternative interpretation of type Foo: Bar; is which is different than the intended meaning. I would be interested in hearing about alternative interpretations.

@Centril

I'll reiterate that type aliases really can be seen as associated types.

They can, but right now associated types are related to traits. impl Trait works everywhere, or almost. If you want to present impl Trait as a kind of associated type, you'll have to introduce two concepts at once. That is, you see impl Trait as a function return type, guess or read up what that is about, then when you see impl Trait in a type alias, you can reuse that knowledge.

Compare that to seeing associated types in a trait definition. In that case you figure it's something that other structs must define or implement. But if you come upon a type Foo: Debug outside of a trait, you won't know what that is. There's no-one to implement it, so is it some kind of forward declaration? Does it have something to do with inheritance, like it does in C++? Is it like an ML module where someone else picks the type? And if you've seen impl Trait before, there's nothing to make a link between them. We write fn foo() -> impl ToString, not fn foo(): ToString.

type Foo = impl Bar; also has understandability issues in that you see = impl Bar and conclude that you can just substitute it at each occurrence where it is used as -> impl Bar

I've said it here before, but that's like thinking that let x = foo(); means you can use x instead of foo(). In any case, it's a detail that someone can quickly look up when needed, but doesn't fundamentally change the concept.

That is, it's easy to figure out what this is about (a deduced type like in -> impl Trait), even if you don't know exactly how it works (what happens when you have conflicting definitions for it). With the other syntax it's hard to even realize what it is.

@Centril

Alright; I propose that we use a different term than incomplete here because to me, it suggests a lack of information.

"incomplete" doesn't have to mean a lack of information, it can mean that something looks like it's supposed to have something else and doesn't.

type Foo: Trait; doesn't look like a complete declaration. It looks like it's missing something. And it seems gratuitously different from type Foo = SomeType<X, Y, Z>;.

Perhaps we're reaching the point that our one-liners on their own can't really bridge this consensus gap between type Inferred: Trait and type Inferred = impl Trait.

Do you think it would be worth putting together an experimental implementation of this feature with any syntax (even the one specified in the RFC) so we can start playing with it in larger programs to see how it fits in context?

@lnicola

[..] impl Trait works everywhere, or almost

Well, Foo: Bound also works almost everywhere ;)

But if you come upon a type Foo: Debug outside of a trait, you won't know what that is.

I think the progression of using it in: trait -> impl -> type alias aids in learning.
Furthermore, I think the inference that "the type Foo implements Debug" is likely from
seeing type Foo: Debug in traits and from generic bounds and it is also correct.

Does it have something to do with inheritance, like it does in C++?

I think the lack of inheritance in Rust needs to be learned at a much earlier stage than when learning the feature we're discussing since this is so fundamental to Rust.

Is it like an ML module where someone else picks the type?

That inference can also be made for type Foo = impl Bar; due to arg: impl Bar where the caller (user) picks the type. To me, the inference that the user picks the type seems less likely for type Foo: Bar;.

I've said it here before, but that's like thinking that let x = foo(); means you can use x instead of foo().

If the language is referentially transparent, you can substitute x for foo(). Up until we add type Foo = impl Foo; into the system, type aliases are afaik referentially transparent. Conversely, if there's already a binding x = foo() available, then other foo() in are replaceable with x.

@joshtriplett

"incomplete" doesn't have to mean a lack of information, it can mean that something looks like it's supposed to have something else and doesn't.

Fair enough; but what is it supposed to have that it doesn't?

type Foo: Trait; doesn't look like a complete declaration.

Looks complete to me. It looks like a judgement that Foo satisfies Trait which is precisely the intended meaning.

@Centril to me the "something missing" is the actual type this is an alias for. That's a bit related to my confusion earlier. It's not that there is no such type, just that that type is anonymous... Using = subtly implies that there is a type and it is always the same type but we can't name it.

I think we are kind of exhausting these arguments though. It would be great to just implement both syntaxes experimentally and see what works best.

@mark-i-m

@Centril to me the "something missing" is the actual type this is an alias for. That's a bit related to my confusion earlier. It's not that there is no such type, just that that type is anonymous... Using = subtly implies that there is a type and it is always the same type but we can't name it.

That's exactly what it feels like to me, as well.

Any chance of tackling the two deferred items soon, plus the issue about lifetime elision? I'd do it myself, but have no idea how!

There's still a lot of confusion around exactly what impl Trait means, and it's not at all obvious. I think the deferred items should definitely wait until we have a clear idea of the exact semantics of impl Trait (which should be coming soon).

@varkor What semantics are unclear? AFAIK nothing about the semantics of impl Trait has changed since RFC 1951, and expanded in 2071.

@alexreg I didn't have any plans to, but here's a rough outline: After parsing has been added, you need to lower the types of statics and consts inside of an existential impl trait context, like is done here for the return types of functions.. However, you'll want to make the DefId in ImplTraitContext::Existential optional, since you don't want your impl Trait to pick up generics from a parent function definition. That should get you a decent bit of the way. You might have an easier time if you base on top of @oli-obk 's existential type PR.

@cramertj: the semantics of impl Trait in the language is entirely restricted to its use in function signatures and it's not true that extending it to other positions has an obvious meaning. I'll say something more detailed about this soon, where most of the conversation seems to be going on.

@varkor

the semantics of impl Trait in the language is entirely restricted to its use in function signatures and it's not true that extending it to other positions has an obvious meaning.

The meaning was specified in RFC 2071.

@cramertj: the meaning in RFC 2071 is ambiguous and permits multiple interpretations of what the phrase "existential type" means there.

TL;DR — I've tried to set out a precise meaning for impl Trait, which I think clarifies details that were, at least intuitively, unclear; along with a proposal for a new type alias syntax.

Existential types in Rust (post)


There's been a lot of discussion going on in the Discord rust-lang chat about the precise (i.e. formal, theoretic) semantics of impl Trait in the last couple of days. I think it's been helpful to clarify a lot of details about the feature and exactly what it is and is not. It also sheds some light on which syntaxes are plausible for type aliases.

I wrote a little summary of some of our conclusions. This provides an interpretation of impl Trait which I think is fairly clean, and precisely describes the differences between argument-position impl Trait and return-position impl Trait (which is not "universally-quantified" vs "existentially-quantified"). There are also some practical conclusions.

In it, I propose a new syntax fulfilling the commonly-stated requirements of an "existential type alias":
type Foo: Bar = _;

Because it's such a complex topic, there's quite a lot that needs to be clarified first though, so I've written it as a separate post. Feedback is very much appreciated!

Existential types in Rust (post)

@varkor

RFC 2071 is ambiguous and permits multiple interpretations of what the phrase "existential type" means there.

How is it ambiguous? I've read your post-- I'm still only aware of one meaning of non-dynamic existential in statics and constants. It behaves the same way that return position impl Trait does, by introducing a new existential type definition per-item.

type Foo: Bar = _;

We discussed this syntax during RFC 2071. As I said there, I like that it demonstrates clearly that Foo is a single inferred type and that it leaves room for non-inferred types that are left existential outside the current module (e.g. type Foo: Bar = u32;). I disliked two aspects of it: (1) it has no keyword, and is therefore harder to search for and (b) it has the same verbosity issue in comparison to type Foo = impl Trait that the abstract type Foo: Bar; syntax has: type Foo = impl Iterator<Item = impl Display>; becomes type Foo: Iterator<Item = MyDisplay> = _; type MyDisplay: Display = _;. I don't think either of these are deal-breakers, but it's not a clear win one way or another IMO.

@cramertj The ambiguity comes up here:

type Foo = impl Bar;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

If Foo were really a type alias for an existential type, then f and g would support different concrete return types. Several people have instinctively read that syntax this way, and in fact some participants in the RFC 2071 syntax discussion only just realized that that's not how the proposal works as part of the recent Discord discussion.

The problem is that, especially in the face of argument-position impl Trait, it's not at all clear where the existential quantifier is meant to go. For arguments it's tightly-scoped; for return position it seems tightly-scoped but turns out to be wider than that; for type Foo = impl Bar both positions are plausible. The _-based syntax nudges toward an interpretation that doesn't even involve "existential," neatly sidestepping this problem.

If Foo were really a type alias for an existential type

(emphasis mine). I read that 'an' as 'a specific' which means f and g would _not_ support different concrete return types, since they refer to the same existential type. I have always seen type Foo = impl Bar; as using the same meaning as let foo: impl Bar;, i.e. introducing a new anonymous existential type; making your example equivalent to

existential type _0: Bar;
type Foo = _0;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

which I would hope is relatively unambiguous.


One issue is that the meaning of "impl Trait in type aliases" has never been specified in an RFC. It is briefly mentioned in RFC 2071's "Alternatives" section, but explicitly discounted because of these inherent teaching ambiguities.

I also feel like I saw some mention that type aliases are already not referentially transparent. I think it was on u.rl.o, but I haven't been able to find the discussion after some searching.

@cramertj
To follow on from @rpjohnst's point, there are multiple interpretations of the semantics of impl Trait, which are all consistent with the current usage in signatures, but have different consequences when extending impl Trait to other locations (I know of 2 other than the one described in the post, but which aren't quite ready for discussion). And I don't think it's true that the interpretation in the post is necessarily the most obvious (I personally didn't see any similar explanation about APIT and RTIP from that perspective).

Regarding the type Foo: Bar = _;, I think perhaps it ought to be discussed again — there's no harm in revisiting old ideas with fresh eyes. Regarding your issues with it:
(1) It has no keyword, but it's the same syntax as type inference anywhere. Searching documentation for "underscore" / "underscore type" / etc. could easily provide a page on type inference.
(2) Yes, that is true. We've been thinking of a solution to this, which I think fits nicely with the underscore notation, which will hopefully be ready to suggest soon.

Like @cramertj I'm not really seeing the argument here.

I just don't see the fundamental ambiguity that @varkor's post describes. I think we've always interpreted "existential type" in Rust as "there exists a _unique_ type that..." and not "there exists at least one type that..." because (as @varkor's post says) the latter is equivalent to "universal types" and therefore the phrase "existential type" would be totally useless if we were intending to allow that interpretation. afaik every RFC on the subject has always assumed universal and existential types were two distinct things. I get that in actual type theory that is what it means and that isomorphism is very mathematically real, but to me that's just an argument that we've been misusing type theory terminology and need to choose some other jargon for this, not an argument that the intended semantics of impl Trait were always unclear and need to be rethought.

The scoping ambiguity that @rpjohnst describes is a serious problem, but every proposed syntax is potentially confusable with either type alises or associated types. Which of those confusions is "worse" or "more likely" is precisely the neverending bikeshed that we've already failed to resolve after several hundred comments. I do like that type Foo: Bar = _; seems to fix type Foo: Bar;'s problem of needing an explosion of several statements to declare any slightly non-trivial existential, but I don't think that's enough to really change the "neverending bikeshed" situation.

What I am convinced of is that whatever syntax we end up with needs to have a keyword other than type, because all of the "just type" syntaxes are too misleading. In fact, maybe don't use type in the syntax _at all_ so there's no way someone could assume they're looking at "a type alias, but more existential somehow".

existential Foo = impl Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }
existential Foo: Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }



md5-b59626c5715ed89e0a93d9158c9c2535



existential Foo: Trait = _;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

It's not obvious to me that any of these completely _prevent_ the misinterpretation that f and g could return two different types implementing Trait, but I suspect this is as close to prevention as we could possibly get.

@Ixrec
The phrase "existential type" is problematic specifically _because_ of the scoping ambiguity. I haven't seen anyone else point out that the scoping is entirely different for the APIT and RPIT. This means that a syntax like type Foo = impl Bar, where impl Bar is an "existential type" is inherently ambiguous.

Yes, the type theory terminology has been misused, a lot. But it's been misused (or at least not explained) in the RFC — so there's ambiguity stemming from the RFC itself.

The scoping ambiguity that @rpjohnst describes is a serious problem, but every proposed syntax is potentially confusable with either type alises or associated types. Which of those confusions is "worse" or "more likely" is precisely the neverending bikeshed that we've already failed to resolve after several hundred comments.

No, I don't think this is true. It's possible to come up with a consistent syntax that doesn't have this confusion. I would venture the bike-shedding is because the two current proposals are bad, so they don't really satisfy anyone.

What I am convinced of is that whatever syntax we end up with needs to have a keyword other than type

I don't think this is necessary either. In your examples, you've invented entirely new notation, which is something you want to avoid in language design wherever possible — otherwise you create a huge language full of inconsistent syntax. You should explore completely new syntax only when there aren't any better options. And I argue that there _is_ a better option.

Aside: on a side note, I think it's possible to move away from "existential types" entirely, making the entire situation clearer, which I or someone else will follow up with soon.

I find myself thinking that a syntax other than type would help as well, precisely because many people interpret type as a simple substitutable alias, which would imply the "potentially different type every time" interpretation.

I haven't seen anyone else point out that the scoping is entirely different for the APIT and RPIT.

I thought the scoping was always an explicit part of the impl Trait proposals, so it didn't need "pointing out". Everything you've said about scoping seems like it's just reiterating what we've already accepted in past RFCs. I get that it's not obvious to everyone from the syntax and that's a problem, but it's not like nobody understood this before. In fact, I thought a huge chunk of the discussion on RFC 2701 was all about what the scoping of type Foo = impl Trait; should be, in the sense of what type inference is and is not allowed to look at.

It's possible to come up with a consistent syntax that doesn't have this confusion.

Are you trying to say type Foo: Bar = _; is that syntax, or do you think we haven't found it yet?

I don't think it's possible to come up with a syntax lacking any similar confusion, not because we're insufficiently creative, but because most programmers are not type theorists. We can probably find a syntax that reduces confusion to a tolerable level, and certainly there are plenty of syntaxes which would be unambiguous to type theory veterans, but we'll never eliminate confusion completely.

you've invented entirely new notation

I thought I just replaced one keyword with another keyword. Are you seeing some additional change I didn't intend?

Come to think of it, since we've been misusing "existential" all this time, that means existential Foo: Trait/= impl Trait probably aren't legitimate syntaxes anymore.

So we need a new keyword to put in front of names that refer to some unknown-to-external-code type... and I'm drawing a blank on this. alias, secret, internal, etc all seem pretty terrible, and unlikely to have any less "uniqueness confusion" than type.

Come to think of it, since we've been misusing "existential" all this time, that means existential Foo: Trait/= impl Trait probably aren't legitimate syntaxes anymore.

Yes, I completely agree — I think we need to move away from the term "existential" entirely* (there have been some tentative ideas for how to do this while still explaining impl Trait well).

*(possibly reserving the term for dyn Trait only)

@joshtriplett, @Ixrec: I agree that the _ notation means you can no longer substitute to the same extent you could before, and if that's a priority to keep, we would need a different syntax.

Bear in mind that _ is already a special case with respect to substitution anyway — it's not just type aliases that this affects: anywhere you can currently use _, you're preventing full referential transparency.

Bear in mind that _ is already a special case with respect to substitution anyway — it's not just type aliases that this affects: anywhere you can currently use _, you're preventing full referential transparency.

Could you walk us through what this means exactly? I wasn't aware of a notion of "referential transparency" that's affected by _.

I agree that the _ notation means you can no longer substitute to the same extent you could before, and if that's a priority to keep, we would need a different syntax.

I'm not sure it's a _priority_. To me it was just the only objective-ish argument that we ever found that seemed to prefer one syntax over the other. But that's all likely to change based on what keywords we can come up with to replace type.

Could you walk us through what this means exactly? I wasn't aware of a notion of "referential transparency" that's affected by _.

Yeah, sorry, I'm throwing words about without explaining them. Let me gather my thoughts, and I'll formulate a more cohesive explanation. It fits in well with an alternative (and potentially more helpful) way to look at impl Trait.

By referential transparency, it is meant that it is possible to substitute a reference for its definition and vice versa without a change in semantics. In Rust, this clearly does not hold at the term level for fn. For example:

fn foo() -> usize {
    println!("ey!");
    42
}

fn main() {
    let bar = foo();
    let baz = bar + bar;
}

if we substitute each occurence of bar for foo() (the definition of bar), then we clearly get a different output.

However, for type aliases, referential transparency hold (AFAIK) at the moment. If you have an alias:

type Foo = Definition;

Then you can do (capture avoiding) substitution of occurrences of Foo for Definition and substitution of occurrences of Definition for Foo without changing the semantics of your program, or its type correctness.

Introducing:

type Foo = impl Bar;

to mean that each occurrence of Foo is the same type means that if you write:

fn stuff() -> Foo { .. }
fn other_stuff() -> Foo { .. }

you can't substitute occurrences of Foo for impl Bar and vice versa. That is, if you write:

fn stuff() -> impl Bar { .. }
fn other_stuff() -> impl Bar { .. }

the return types won't unify with Foo. Thus referential transparency is broken for type aliases by introducing impl Trait with the semantics of RFC 2071 inside of them.

On referential transparency and type Foo = _;, to be continued... (by @varkor)

I find myself thinking that a syntax other than type would help as well, precisely because many people interpret type as a simple substitutable alias, which would imply the "potentially different type every time" interpretation.

Good point. But doesn't the = _ assignment bit imply that it's only a single type?

I've written this before, but...

Re referential transparency: I think it's more useful to look at type as a binding (like let) instead of C preprocessor-like substitution. Once you look at it that way, type Foo = impl Trait means exactly what it seems.

I imagine beginners will be less likely to think of impl Trait as existential vs. universtal types, but as "a thing that impls a Trait. If they want to know more, they can read theimpl Trait` documentation. Once you change the syntax, you lose the connection between it and the existing feature with not much benefit. _You're only replacing one potentially misleading syntax with another._

Re type Foo = _, it overloads _ with a completely unrelated meaning. It can also seem tricky to find in the documentation and/or Google.

@lnicola You could just as well go with const bindings instead of let bindings, where the former is referentially transparent. Picking let (which happens to not be referentially transparent inside fn) is an arbitrary choice that I don't think is particularly intuitive. I think the intuitive view of type aliases is that they are referentially transparent (even if that word is not used) because they are aliases.

I'm also not looking at type as C preprocessor substitution because it has to be capture avoiding and respect generics (no SFINAE). Instead, I'm thinking of type precisely as I would a binding in a language like Idris or Agda where all bindings are pure.

I imagine beginners will be less likely to think of impl Trait as existential vs. universtal types, but as "a thing that impls a Trait

That seems like a distinction without a difference to me. The jargon "existential" is not used, but I believe the user is intuitively linking it to the same concept as that of an existential type (which is nothing more than "some type Foo that impls Bar" in the context of Rust).

Re type Foo = _, it overloads _ with a completely unrelated meaning.

How so? type Foo = _; here aligns with the use of _ in other contexts where a type is expected.
It means "infer the real type", just as when you write .collect::<Vec<_>>().

It can also seem tricky to find in the documentation and/or Google.

Shouldn't be that difficult? "type alias underscore" should hopefully bring up the wanted result...?
Doesn't seem any different than searching for "type alias impl trait".

Google doesn't index special characters. If my StackOverflow question has an underscore in it, Google won't automatically index that for queries that contain the word underscore

@Centril

How so? type Foo = _; here aligns with the use of _ in other contexts where a type is expected.
It means "infer the real type", just as when you write .collect::>().

But this feature doesn't infer the type and give you a type alias for it, it creates an existential type which (outside of some limited scope like module or crate) doesn't unify with "the real type".

Google doesn't index special characters.

This is no longer true (though possibly whitespace-dependent..?).

But this feature doesn't infer the type and give you a type alias for it, it creates an existential type which (outside of some limited scope like module or crate) doesn't unify with "the real type".

The suggested semantics of type Foo = _; is as an alternative to having an existential type alias, based entirely on inference. If that wasn't entirely clear, I'm going to follow up soon with something that should explain the intentions a bit better.

@iopq In addition to @varkor's note about recent changes, I'd also like to add that for other search engines, it is always possible that official documentation and such explicitly use the literal word "underscore" in conjunction with type such that it becomes searchable.

You still won't get good results with _ in your query, for whatever reason. If you search underscore, you get things with the word underscore in them. If you search _ you get everything that has an underscore, so I don't even know if it's relevant

@Centril

Picking let (which happens to not be referentially transparent inside fn) is an arbitrary choice that I don't think is particularly intuitive. I think the intuitive view of type aliases is that they are referentially transparent (even if that word is not used) because they are aliases.

Sorry, I still can't wrap my head around this because my intuition is completely backwards.

For example, if we have type Foo = Bar, my intuition says:
"We're declaring Foo, which becomes the same type as Bar."

Then, if we write type Foo = impl Bar, my intuition says:
"We're declaring Foo, which becomes a type that implements Bar."

If Foo is just a textual alias for impl Bar, then that'd be super unintuitive to me. I like thinking of this as textual vs semantic aliases.

So if Foo can be replaced with impl Bar anywhere it appears, that's a textual alias, to me most reminiscent of macros and metaprogramming. But if Foo was assigned a meaning at the point of declaration and can be used in multiple places with that original meaning (not contextual meaning!), that's a semantic alias.

Also, I fail to understand the motivation behind contextual existential types anyhow. Would they ever be useful, considering that trait aliases can achieve the exact same thing?

Perhaps I find referential transparency unintuitive because of my non-Haskell background, who knows... :) But in any case, it's definitely not the kind of behavior I'd expect in Rust.

@Nemo157 @stjepang

If Foo were really a type alias for an existential type

(emphasis mine). I read that 'an' as 'a specific' which means f and g would not support different concrete return types, since they refer to the same existential type.

This is a misuse of the term "existential type," or at least a way that is at odds with @varkor's post. type Foo = impl Bar can appear to make Foo an alias for the type ∃ T. T: Trait- and if you substitute ∃ T. T: Trait everywhere you use Foo, even non-textually, you can get a different concrete type in each position.

The scoping of this ∃ T quantifier (expressed in your example as existential type _0) is the thing in question. It's tight like this in APIT- the caller can pass any value that satisfies ∃ T. T: Trait. But it's not in RPIT, and not in RFC 2071's existential type declarations, and not in your desugaring example- there, the quantifier is farther out, at the whole-function or whole-module level, and you deal with the same T everywhere.

Thus the ambiguity- we already have impl Trait placing its quantifier in different places depending on its position, so which one should we expect for type T = impl Trait? Some informal polls, as well as some after-the-fact-realizations by participants in the RFC 2071 thread, prove that it's not clear one way or the other.

This is why we want to move away from the interpretation of impl Trait as anything at all to do with existentials, and instead describe its semantics in terms of type inference. type T = _ does not have the same sort of ambiguity- there's still the surface-level "can't copy-paste the _ in place of T," but there's no longer "the single type that T is an alias of can mean multiple concrete types." (The opaque/doesn't-unify behavior is the thing @varkor is talking about following up on.)

referential transparency

Just because a type alias currently being compatible with referential transparency, doesn't mean people expect the feature to follow it.

As an example, the const item is referential transparent (mentioned in https://github.com/rust-lang/rust/issues/34511#issuecomment-402520768), and that actually caused confusion to new and old users (rust-lang-nursery/rust-clippy#1560).

So I think for a Rust programmer referential transparency isn't the first thing they would think of.

@stjepang @kennytm I'm not saying that everyone will expect that type aliases with type Foo = impl Trait; will act in a referentially transparent manner. But I think a non-trivial amount of users will, as evidenced by confusions in this thread and elsewhere (what @rpjohnst is referring to...). This is a problem, but perhaps not an insurmountable one. It is something to keep in mind tho as we move forward.

My current thinking on what should be done in this matter has moved in line with @varkor and @rpjohnst.

re: referential transparency

type Foo<T> = (T, T);

type Bar = Foo<impl Copy>;   // not equivalent to (impl Copy, impl Copy)

that is to say, even generating new types at every instance is not referentially transparent in the context of generic type aliases.

@centril I raise my hand when it comes to expecting referential transparency for Foo in type Foo = impl Bar;. With type Foo: Bar = _; however, I would not expect referential transparency.

It's also possible that we could extend return-position impl Trait to support multiple types, without any sort of enum impl Trait-like mechanism, by monomorphizing (pieces of) the caller. This strengthens the "impl Trait is always existential" interpretation, brings it closer in line with dyn Trait, and suggests an abstract type syntax that doesn't use impl Trait at all.

I wrote this up on internals here: https://internals.rust-lang.org/t/extending-impl-trait-to-allow-multiple-return-types/7921

Just a note for when we stabilise the new existential types - "existential" was always intended to be a temporary keyword (according to the RFC) and (IMO) is terrible. We must come up with something better before stabilising.

The talk about “existential” types does not seem to be clearing things. I would say that impl Trait stands for a specific, inferred type implementing Trait. Described that way, type Foo = impl Bar is clearly a specific, always the same, type—and that is also the only interpretation that is actually useful: so it can be used in other contexts besides the one from which it was inferred, like in structs.

In this sense, it would make sense to also write impl Trait as _ : Trait.

@rpjohnst,

It's also possible that we could extend return-position impl Trait to support multiple types

That would make it strictly less useful IMO. The point of aliases to impl types is that a function can be defined as returning impl Foo, but the specific type still propagated through the program in other structs and stuff. That would work if the compiler implicitly generated suitable enum, but not with monomorphisation.

@jan-hudec Those ideas have come up in discussion on Discord, and there are some issues, primarily based around the fact that the current interpretation of return-position and argument-position impl Trait are inconsistent.

Making impl Trait stand for a specific inferred type is a good option, but for it to fix that inconsistency, it must be a different kind of type inference than Rust has today- it must infer polymorphic types so that it can preserve the current behavior of argument-position impl Trait. This is probably the most straightforward way to go, but it's not as simple as you say.

For example, once impl Trait means "use this new type of inference to find a polymorphic-as-possible type that implements Trait," type Foo = impl Bar starts to imply things about modules. The RFC 2071 rules around how to infer an abstract type say that all uses must independently infer the same type, but this polymorphic inference would at least imply that more is possible. And if we ever got parametrized modules (even just over lifetimes, a far more plausible idea), there would be questions around that interaction.

There's also the fact that some people will always interpret the type Foo = impl Bar syntax as an alias for an existential, regardless of whether they understand the word "existential" and regardless of how we teach it. So picking an alternative syntax, even if it happens to work out with the inference-based interpretation, is probably still a good idea.

Further, while the _: Trait syntax is actually what inspired the discussion around the inference-based interpretation in the first place, it does not do what we want. First, the inference implied by _ is not polymorphic, so that's a bad analogy to the rest of the language. Second, _ implies that the actual type is visible elsewhere, while impl Trait is specifically designed to hide the actual type.

Finally, the reason I wrote that monomorphization proposal was from the angle of finding another way to unify the meaning of argument and return-position impl Trait. And while yes, it does mean that -> impl Trait no longer guarantees a single concrete type, we don't currently have a way to take advantage of that anyway. And the proposed solutions are all annoying workarounds- extra boilerplate abstract type tricks, typeof, etc. Forcing everyone who wants to rely on single-type behavior to also name that single type via the abstract type syntax (whatever it may be) is arguably a benefit overall.

Those ideas have come up in discussion on Discord, and there are some issues, primarily based around the fact that the current interpretation of return-position and argument-position impl Trait are inconsistent.

Personally, I don't find this inconsistency to be a problem in practice. The scope in which concrete types are determined for argument position vs return position vs type position seem to work out fairly intuitively.

I have a function where the caller decides its return type. Of course, I can't use impl Trait there. It's not as intuitive as you imply until you understand the difference.

Personally, I don't find this inconsistency to be a problem in practice.

Indeed. What this suggests to me is not that we should ignore the inconsistency, but that we should re-explain the design so that it's consistent (for example, by explaining it as polymorphic type inference). This way, future extensions (RFC 2071, etc.) can be checked against the new, consistent interpretation to prevent things from becoming confusing.

@rpjohnst

Forcing everyone who wants to rely on single-type behavior to also name that single type via the abstract type syntax (whatever it may be) is arguably a benefit overall.

For some cases I agree with that sentiment, but it doesn't work with closures or generators, and is unergonomic for a lot of cases where you don't care what the type is and all you care about is that it implements a certain trait, e.g. with iterator combinators.

@mikeyhew You misunderstand me- it works fine for closures or other unnameable types, because I'm talking about inventing a name via RFC 2071 abstract type syntax. You have to invent a name regardless if you're going to use the single type anywhere else.

@rpjohnst oh I see, thanks for clarifying

Waiting for let x: impl Trait anxiously.

As another vote for let x: impl Trait it'll simplify some of the futures examples, here's an example example, currently it is using a function just to get the ability to use impl Trait:

fn make_sink_async() -> impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> { // ... }

instead this could be written as a normal let binding:

let future_sink: impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> = // ...;

I can mentor someone through implementing let x: impl Trait if desired. It's not impossibly hard to do, but definitely not easy either. An entry point:

Similarly to how we visit the return type impl Trait in https://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159 we need to visit the type of locals in https://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159 and make sure their newly generated existential items are returned together with the local.

Then, when visiting the type of locals, be sure to set ExistentialContext to Return to actually enable it.

This should already get us very far. Not sure if all the way, it's not 100% like return position impl trait, but mostly should behave like it.

@rpjohnst,

Those ideas have come up in discussion on Discord, and there are some issues, primarily based around the fact that the current interpretation of return-position and argument-position impl Trait are inconsistent.

Takes us back to the scopes you talked about in your article. And I think they actually correspond to the enclosing “parenthesis”: for argument position it is the argument list, for return position it is the function—and for the alias it would be the scope in which the alias is defined.

I've opened an RFC proposing a resolution to the existential type concrete syntax, based on the discussion in this thread, the original RFC and synchronous discussions: https://github.com/rust-lang/rfcs/pull/2515.

The current existential type implementation can't be used to represent all current return position impl Trait definitions, since impl Trait captures every generic type argument even if unused it should be possible to do the same with existential type, but you get unused type parameter warnings: (playground)

fn foo<T>(_: T) -> impl ::std::fmt::Display {
    5
}

existential type Bar<T>: ::std::fmt::Display;
fn bar<T>(_: T) -> Bar<T> {
    5
}

This can matter because the type parameters can have internal lifetimes that restrict the lifetime of the returned impl Trait even though the value itself is unused, remove the <T> from Bar in the playground above to see that the call to foo fails but bar works.

The current existential type implementation can't be used to represent all current return position impl Trait definitions

you can, it's just very inconvenient. You can return a newtype with a PhantomData field + actual data field and implement the trait as forwarding to the actual data field

@oli-obk Thanks for the additional advice. With your previous advice and some from @cramertj, I could probably have a go at it shortly.

@fasihrana @Nemo157 See above. Maybe in a few weeks! :-)

Can someone clarify that the behavior of existential type not capturing type parameters implicitly (that @Nemo157 mentioned) is intentional and will stay as it is? I like it because it solves #42940

I implemented it this way very much on purpose

@Arnavion Yes, this is intentional, and matches the way that other item declarations (e.g. nested functions) work in Rust.

Was interaction between existential_type and never_type already discussed?

Maybe ! should be able to fill in any existential type regardless of traits involved.

existential type Mystery : TraitThatIsHardToEvenStartImplementing;

fn hack_to_make_it_compile() -> Mystery { unimplemented!() }

Or shall there be some special untouchable type serving as type-level unimplemented!() that is able to automatically satisfy any existential type?

@vi I think that would fall under the general "never type should implement all traits without any non-default non-self methods or associated types". I don't know where that would be tracked, though.

Is there a plan to extend support to trait method return types soon?

existential type already works for trait methods. Wrt impl Trait, is that even covered by an RFC?

@alexreg I believe that requires GATs to be able to desugar to an anonymous associated type when you have something like fn foo<T>(..) -> impl Bar<T> (becomes roughly -> Self::AnonBar0<T>).

@Centril did you mean to do the <T> on impl Bar there? The implicit type capture behaviour of impl Trait means that you get the same need for GATs even with something like fn foo<T>(self, t: T) -> impl Bar;.

@Nemo157 no sorry I did not. But your example illustrates the problem even better. Thank you :)

@alexreg I believe that requires GATs to be able to desugar to an anonymous associated type when you have something like fn foo(..) -> impl Bar (becomes roughly -> Self::AnonBar0).

Ah, I see. To be honest, it doesn't sound strictly necessary, but it's certainly one way of implementing it. The lack of movement on GATs is a bit worrying to me though... haven't heard anything in a long time.

Triage: https://github.com/rust-lang/rust/pull/53542 has been merged, so the ticky boxes for {let,const,static} foo: impl Trait can be checked, I think.

Will I ever be able to write:

trait Foo {
    fn GetABar() -> impl Bar;
}

??

Probably not. But there are ongoing plans to preparing everything so we might get

trait Foo {
    type Assoc: Bar;
    fn get_a_bar() -> Assoc;
}

impl Foo for SomeType {
    fn get_a_bar() -> impl Bar {
        SomeThingImplingBar
    }
}

You can experiment with this feature on nightly in the form of

impl Foo for SomeType {
    existential type Assoc;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBar
    }
}

A good start to get more info about this is https://github.com/rust-lang/rfcs/pull/2071 (and everything linked from it)

@oli-obk on rustc 1.32.0-nightly (00e03ee57 2018-11-22), I need to also give the trait bounds for existential type to work in an impl block like that. Is that expected?

@jonhoo being able to specify the traits is useful because you can provide more than just the required traits

impl Foo for SomeDebuggableType {
    existential type Assoc: Bar + Debug;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBarAndDebug
    }
}

fn use_debuggable_foo<F>(f: F) where F: Foo, F::Assoc: Debug {
    println!("bar is: {:?}", f.get_a_bar())
}

The required traits could be implicitly added to an existential associated type, so you only need bounds there when extending them, but personally I'd prefer the local documentation of having to put them in the implementation.

@Nemo157 Ah, sorry, what I meant is that currently you _must_ have bounds there. That is, this won't compile:

impl A for B {
    existential type Assoc;
    // ...
}

whereas this will:

impl A for B {
    existential type Assoc: Debug;
    // ...
}

Oh, so even in the case where a trait requires no bounds of the associated type, you must still give a bound to the existential type (which may be empty) (playground):

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

This seems like an edge-case to me, having a bound-less existential type means it provides _no_ operations to users (other than auto-traits), so what could it be used for?

Also of note is there is no way to do the same thing with -> impl Trait, -> impl () is a syntax error and -> impl on its own gives error: at least one trait must be specified; if the existential type syntax becomes type Assoc = impl Debug; or similar then it seems like there would be no way to specify the associated type without at least one trait bound.

@Nemo157 yeah, I only realized because I tried literally the code you suggested above, and it didn't work :p I sort of assumed that it would infer the bounds from the trait. For example:

trait Foo {
    type Assoc: Future<Output = u32>;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc;
}

It seemed reasonable to not have to specify Future<Output = u32> a second time, but that doesn't work. I assume that existential type Assoc: ; (which also seems like super weird syntax) won't do that inference either?

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

This seems like an edge-case to me, having a bound-less existential type means it provides _no_ operations to users (other than auto-traits), so what could it be used for?

Couldn't these be used for consumption in the same trait implementation? Something like this:

trait Foo {
    type Assoc;
    fn create_constructor() -> Self::Assoc;
    fn consume(marker: Self::Assoc) -> Self;
    fn consume_box(marker: Self::Assoc) -> Box<Foo>;
}

It's a bit contrived, but it could be useful - I could imagine a situation where some preliminary part needs to be constructed before the real struct for lifetime reasons. Or it could be something like:

trait MarkupSystem {
    type Cache;
    fn create_cache() -> Cache;
    fn translate(cache: &mut Self::Cache, input: &str) -> String;
}

In both these cases existential type Assoc; would be useful.

What is the proper way of defining associated types for impl Trait?

For example, if I have an Action trait and I want to ensure the implementation of the trait's associated type is sendable, can I do something like this:

pub trait Action {
    type Result;
    fn call(&self) -> Self::Result;
}

impl MyStruct {
    pub fn new(name: String) -> impl Action 
    where 
        Return::Result: Send //This Return should be the `impl Action`
    {
        ActionImplementation::new()
    }
}

Is something this currently not possible?

@acycliczebra I think the syntax for that is -> impl Action<Result = impl Send> - this is the same syntax as, for example, -> impl Iterator<Item = u32> just using another anonymous impl Trait type.

Has there been any discussion about extending impl Trait syntax to things like struct fields? For example, if I'm implementing a wrapper around a specific iterator type for my public interface:

struct Iter<'a> {
    inner: std::collections::hash_map::Iter<'a, i32, i32>,
}

It would be useful in those situations where I don't really care about the actual type as long as it satisfies certain trait bounds. This example is simple, but I've encountered situations in the past where I'm writing very long types with a bunch of nested type parameters, and it's really unnecessary because I really don't care about anything except that this is an ExactSizeIterator.

However IIRC, I don't think there is a way to specify multiple bounds with impl Trait at the moment, so I would lose some useful things like Clone.

@AGausmann The latest discussion on the subject is in https://github.com/rust-lang/rfcs/pull/2515. This would allow you to say type Foo = impl Bar; struct Baz { field: Foo } .... I think we may want to consider field: impl Trait as sugar for that after stabilizing type Foo = impl Bar;. It does feel like a reasonable macro-friendly convenience-extension.

@Centril,

I think we may want to consider field: impl Trait as sugar

I don't think this would be reasonable. A struct field still has to have a concrete type, so you have to tell the compiler the return of which function it is tied to. It could infer it, but if you have multiple functions than it would not be that easy to find which one it is—and the usual policy of Rust is to be explicit in such cases.

It could infer it, but if you have multiple functions than it would not be that easy to find which one it is

You would bubble up the requirement on defining uses to the parent type. It would then be all those functions in the same module which return the parent type. Doesn't seem all that difficult to me to find. I think however we want to settle the story on type Foo = impl Bar; before moving ahead with extensions.

I think I found a bug in the current existential type implementation.


Code

trait Collection {
    type Element;
}
impl<T> Collection for Vec<T> {
    type Element = T;
}

existential type Existential<T>: Collection<Element = T>;

fn return_existential<I>(iter: I) -> Existential<I::Item>
where
    I: IntoIterator,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}


Error

error: type parameter `I` is part of concrete type but not used in parameter list for existential type
  --> src/lib.rs:16:1
   |
16 | / {
17 | |     let item = iter.into_iter().next().unwrap();
18 | |     vec![item]
19 | | }
   | |_^

error: defining existential type use does not fully define existential type
  --> src/lib.rs:12:1
   |
12 | / fn return_existential<I>(iter: I) -> Existential<I::Item>
13 | | where
14 | |     I: IntoIterator,
15 | |     I::Item: Collection,
...  |
18 | |     vec![item]
19 | | }
   | |_^

error: could not find defining uses
  --> src/lib.rs:10:1
   |
10 | existential type Existential<T>: Collection<Element = T>;
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

playground

You can find this on stackoverflow, too.

I'm not 100% sure we can support this case out of the box, but what you can do is rewrite the function to have two generic parameters:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=b4e53972e35af8fb40ffa9a735c6f6b1

fn return_existential<I, J>(iter: I) -> Existential<J>
where
    I: IntoIterator<Item = J>,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

Thanks!
Yup, this is what I did as posted on the stackoverflow post:

fn return_existential<I, T>(iter: I) -> Existential<T>
where
    I: IntoIterator<Item = T>,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

Are there plans for impl Trait to be available within a context of trait?
Not only as associated type, but also as return value in methods.

impl trait in traits is a separate feature to the ones being tracked here, and does not presently have an RFC. There is a fairly long history of designs in this space, and further iteration is being held off until the implementation of 2071 (existential type) is stabilized, which is blocked on implementation issues as well as unresolved syntax (which has a separate RFC).

@cramertj The syntax is nearly resolved. I believe the main blocker is GAT now.

@alexreg: https://github.com/rust-lang/rfcs/pull/2515 is still waiting on @withoutboats.

@varkor Yeah, I'm just being optimistic they'll see the light with that RFC soon. ;-)

Will something like the following be possible?

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{
    let mut s = MyStruct {};
    cb(&mut s)
}

You can do this now, although only with a hint function to specify the concrete type of Interface

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{

    fn hint(x: &mut MyStruct) -> &mut Interface { x }

    let mut s = MyStruct {};
    cb(hint(&mut s))
}

How would you write it if the callback would be able to choose its argument type? Actually nvm, I guess you could solve that one via a normal generic.

@CryZe What you're looking for is unrelated to impl Trait. See https://github.com/rust-lang/rfcs/issues/2413 for all I know about it.

It would potentially look something like that :

trait MyTrait {}

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: for<I: Interface> FnOnce(&mut I) -> U
{
    let mut s = MyStruct {};
    cb(hint(&mut s))
}

@KrishnaSannasi Ah, interesting. Thanks!

Is this supposed to work?

#![feature(existential_type)]

trait MyTrait {
    type AssocType: Send;
    fn ret(&self) -> Self::AssocType;
}

impl MyTrait for () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

impl<'a> MyTrait for &'a () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

trait MyLifetimeTrait<'a> {
    type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType;
}

impl<'a> MyLifetimeTrait<'a> for &'a () {
    existential type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType {
        *self
    }
}

Do we have to keep existential keyword in language for existential_type feature?

@jethrogb Yes. The fact that it currently doesn't is a bug.

@cramertj Ok. Should I file a separate issue for that or is my post here enough?

Filing an issue would be great, thanks! :)

Do we have to keep existential keyword in language for existential_type feature?

I think the intention is to immediately deprecate this when the type-alias-impl-trait feature is implemented (i.e., put in a lint) and eventually remove it from the syntax.

Someone can maybe clarify though.

Closing this in favor of a meta-issue which tracks impl Trait more generally: https://github.com/rust-lang/rust/issues/63066

not a single good example anywhere on how to use impl Trait,, very sad

Was this page helpful?
0 / 5 - 0 ratings