rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
97.35k stars 12.59k forks source link

Tracking issue for `impl Trait` (RFC 1522, RFC 1951, RFC 2071) #34511

Closed aturon closed 5 years ago

aturon commented 8 years ago

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:

J-F-Liu commented 7 years ago

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.

eddyb commented 7 years ago

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

Kixunil commented 7 years ago

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

J-F-Liu commented 7 years ago

@ is also use in pattern matching, not removed from the language.

Kixunil commented 7 years ago

The thing I had in mind is that AFAIK sigils were over-used.

matklad commented 7 years ago

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?

mglagla commented 7 years ago

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> {...}
J-F-Liu commented 7 years ago

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

Kerollmops commented 7 years ago

I prefer the impl keyword syntax because when you read documentation rapidly this allow less way to misread prototypes. What do you think ?

konstin commented 7 years ago

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.

matklad commented 7 years ago

This paper by Stroustrup discusses similar syntactic choices for C++ concepts in section 7: http://www.stroustrup.com/good_concepts.pdf

notriddle commented 7 years ago

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.

J-F-Liu commented 7 years ago

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.

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

notriddle commented 7 years ago

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.

cramertj commented 7 years ago

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

crlf0710 commented 7 years ago

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?

NeoLegends commented 7 years ago

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.

pthariensflame commented 7 years ago

Yeah, sticking with existing keywords seems ideal to me; I'd like to see impl for existentials and for for universals.

Rufflewind commented 7 years ago

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

fn foo<F, R>(callback: F) -> R
    where F: FnOnce(impl SomeTrait) -> R {
    callback(create_something())
}

Right now, this is not a necessary feature, since you can always put a concrete time for impl SomeTrait, which hurts readability but is otherwise not a big deal.

But if RFC 1522 feature stabilizes, then it would be impossible to assign a type signature to programs such the above if create_something results in impl SomeTrait (at least without boxing it). I think this is problematic.

eddyb commented 7 years ago

@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:

solson commented 7 years ago

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

solson commented 7 years ago

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:

Rufflewind commented 7 years ago

Naively treating impl as technically only meaning universal or only meaning existential and letting the logic work itself out doesn't actually work out. You would get either this:

@solson To me, a “naive” translation would result in the existential quantifiers right next to the type being quantified. Hence

(impl MyTrait)

is just syntactic sugar for

(exists<T: MyTrait> T)

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

fn(exists<T: ArgTrait> T) -> (exists<R: ReturnTrait> R)

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

for<T: ArgTrait> fn(T) -> (exists<R: ReturnTrait> R)

So even though T is always existential relative to itself, it appears as universal relative to the whole function type.


IMO, I think impl may as well become the de facto keyword for existential types. In the future, perhaps one could conceivably construct more complicated existential types like:

(impl<T: MyTrait> (Vec<T>, T))

in analogy to universal types (via HRTB)

(for<'a> FnOnce(&'a T))
solson commented 7 years ago

@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).

Rufflewind commented 7 years ago

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

aturon commented 7 years ago

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.

bstrie commented 7 years ago

Worth noting that https://github.com/rust-lang/rfcs/pull/1951 has been accepted.

sophiajt commented 7 years ago

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.

kennytm commented 7 years ago

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?

alvitawa commented 7 years ago

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 commented 7 years ago

@alvitawa See https://github.com/rust-lang/rfcs/pull/2071

alvitawa commented 7 years ago

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

nikomatsakis commented 7 years ago

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:

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. ;)

eddyb commented 7 years ago

@nikomatsakis I believe a lot of that is similar to what I've told @cramertj, but more fleshed out!

nikomatsakis commented 6 years ago

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

cuviper commented 6 years ago

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.

oberien commented 6 years ago

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
cramertj commented 6 years ago

@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);
}
ErichDonGubler commented 6 years ago

Looks like this is closed now:

ICEs when interacting with elision

mikeyhew commented 6 years ago

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.

nikomatsakis commented 6 years ago

@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;
            }
        })
    }
}
scottmcm commented 6 years ago

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?

cramertj commented 6 years ago

@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)
    }
}
alexreg commented 6 years ago

@cramertj Wait, abstract types are already in nightly?! Where's the PR?

cramertj commented 6 years ago

@alexreg No, they are not.

ExpHP commented 6 years ago

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.

nikomatsakis commented 6 years ago

@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

runiq commented 6 years ago

Tiny bit of triage for people who like checkboxes:

cramertj commented 6 years ago

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

rfcbot commented 6 years ago

Team member @cramertj has proposed to merge this. The next step is review by the rest of the tagged teams:

No concerns currently listed.

Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.