New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Finalize syntax and parameter scoping for `impl Trait`, while expanding it to arguments #1951

Merged
merged 4 commits into from May 24, 2017

Conversation

@aturon
Copy link
Member

aturon commented Mar 15, 2017

This RFC proposes several steps forward for impl Trait:

  • Settling on a particular syntax design, resolving questions around the
    some/any proposal and others.

  • Resolving questions around which type and lifetime parameters are considered
    in scope for an impl Trait.

  • Adding impl Trait to argument position (where it is understood as anonymous, bounded generics)

The first two proposals, in particular, put us into a position to stabilize the
current version of the feature in the near future.

Rendered

[edit: updated rendered link —mbrubeck]

@aturon aturon added the T-lang label Mar 15, 2017

@aturon aturon self-assigned this Mar 15, 2017

@aturon

This comment has been minimized.

Copy link
Member

aturon commented Mar 15, 2017

@aturon

This comment has been minimized.

Copy link
Member

aturon commented Mar 15, 2017

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Mar 15, 2017

One thing not addressed in the RFC is impl Trait in higher order function argument position, that is:

fn some_func(f: impl Fn(impl Debug) -> String)

We've talked about wanting this case (and only this case) to have an existential or possibly higher-rank semantics, rather than universal - a sort of contravariance. Do you think that's not a good idea anymore, or is it just missing from the RFC?

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Mar 15, 2017

Thinking more zbout it now, though, I wonder if that's such a good idea. There are other traits for which that would be ideal - From for example:

fn func(arg: impl From<impl ToString>)

Since we can't make a correct determination of what you want in every case, leaving these all as universals (rather than a special case for Fn args) seems reasonable. Users will instead (someday) write:

fn some_func(f: impl for<T: Debug> Fn(T) -> String)

// maybe even
fn some_func(f: impl<T: Debug> Fn(T) -> String)
```
Here `impl Trait` is used for a type whose identity isn't important, where
introducing an associated type is overkill.

This comment has been minimized.

@cramertj

cramertj Mar 15, 2017

Member

If I understand correctly, this proposal would only allow returning impl Trait when implementing a trait that uses this new syntax. This prevents the use of impl Trait return types when implementing trait functions that return an associated type.

This means, for example, that the Service trait from Tokio (link) would have to be changed to the following in order to support Services returning impl Futures:

pub trait Service {

    /// Requests handled by the service.
    type Request;

    /// Responses given by the service.
    type Response;

    /// Errors produced by the service.
    type Error;

    // NOTE: No more `type Future`

    /// Process the request and return the response asynchronously.
    fn call(&self, req: Self::Request) -> impl Future<Item = Self::Response, Error = Self::Error>;
}

But now because the Future associated type has been removed, it's impossible to place bounds on the output of a Service. For example, when writing a fn foo<S>(s: S) where S: Service, it's no longer possible to constrain <S as Service>::Future: 'static + Sync + Clone + etc.

Similarly, for the Iterable trait provided, it's impossible to write fn use_exact_size_iterable<I>(x: I) where I: Iterable, <I as Iterable>::Iterator: ExactSizeIterator.

Because of these restrictions, library authors would be forced to choose whether they want their trait to be usable with impl Trait, or whether they want their return types to be boundable. This would be a very difficult and conflicting decision, and either choice would undoubtedly result in frustration for users of the trait who either want to use impl Trait or place bounds on return types.

This comment has been minimized.

@withoutboats

withoutboats Mar 15, 2017

Contributor

Its worth noting that there are impls that can't be written with the associated type form - any type parameterized by a closure, for example, because closures have anonymous types.

Likely we someday want to support something like Serivce::call::Return as a type.

This comment has been minimized.

@cramertj

cramertj Mar 15, 2017

Member

@withoutboats Right-- it's clear that impl Trait allows not only more convenient trait impls, but impls that weren't possible before. It seems odd to force trait-writers to choose to either allow those impls OR allow writing bounds-- neither is fully expressive.

I'd prefer the syntax for trait declaration go unchanged (making all current trait impls forwards-compatible with impl Trait) and that users be able to specify associated types using something like the suggested "fully-explicit" syntax.

This comment has been minimized.

@withoutboats

withoutboats Mar 15, 2017

Contributor

I'd prefer the syntax for trait declaration go unchanged (making all current trait impls forwards-compatible with impl Trait) and that users be able to specify associated types using something like the suggested "fully-explicit" syntax.

Can you explain what you mean?

This comment has been minimized.

@cramertj

cramertj Mar 15, 2017

Member

WRT forwards-compatible traits: under this proposal, traits whose functions return associated types are not usable with impl Trait return types (if I understand the proposal correctly). In order for a trait impl to use impl Trait in the return type of a function, the trait function would need to be explicitly declared with an impl Trait return type, rather than returning an associated type.

WRT my preference: leave trait declaration alone, and allow trait impls to use existentials as associated types. I'm not ready to propose my own syntax, but using the one suggested at the end of this RFC, it would look like this:

struct MyIter;
abstype MyItem: Fn(i32) -> i32;
impl Iterator for MyIter {
    type Item = MyItem;
    fn next(&mut self) -> Option<MyItem> {
        Some(|x| x + 5)
    }
}

Note that this impl would be impossible to write under the current proposal, as it requires the use of existential return types in traits that are already stabilized (and thus couldn't be changed to have their associated type removed).

This comment has been minimized.

@comex

comex Mar 17, 2017

Quick note that foo::Return is syntactically problematic, because Rust lets you define a value-like thing (such as a fn) and a type-like thing (including a module) with the same name. So this would be perfectly legal:

fn A() -> impl Trait { … }
mod A { pub type Return = ...; }
// what does A::Return refer to?

(Alternately the second A could be a struct.)

One potential solution would be using a keyword, foo::return, but that doesn't generalize if for any reason there's a need for additional pseudo-associated-types on fns.

This comment has been minimized.

@solson

solson Mar 17, 2017

Member

Quick note that foo::Return is syntactically problematic

Maybe we could implement typeof and use <typeof(foo) as FnOnce>::Output? (The typeof keyword is already reserved.)

@cramertj

This comment has been minimized.

Copy link
Member

cramertj commented Mar 15, 2017

Is there a plan to support impl Trait in let bindings, or was this purposefully excluded for some reason?
Example: let x: impl Fn(i32) -> i32 = |x| x + 1;

@tomaka

This comment has been minimized.

Copy link

tomaka commented Mar 15, 2017

If you write this:

trait Foo {
    fn foo(&self) -> impl Trait;
}

Is there then some way to use a Box<Foo>? Or is it strictly forbidden?

(EDIT: I mean, because you can't write Box<Foo<??? = u8>>)

@krdln

This comment has been minimized.

Copy link
Contributor

krdln commented Mar 15, 2017

One important question is: will people find it easier to understand and use impl Trait, or something like some Trait and any Trait? Having an explicit split may make it easier to understand what's going on.

I think this is actually the most important question that should matter when deciding on any/some vs. impl. Even if we end up not accepting any on return position or don't even allow any sugar for argument positions, the -> some syntax might still be beneficial in terms of readability and teachability. I won't hide I'm now a fan of this syntax (despite being sceptical before) – one nice thing about it is that it's self-explanatory – with impl you have to teach that a function can accept any implementation of a trait and return some instance of a trait. any+some explains this directly in the code itself.

Regarding dropping the keyword altogether – I also consider a current (bare Trait for dynamic dispatch) syntax a mistake, but solely because it allows an entity from a trait space to be expressed in a type space without any additional notation. This is acceptable in Java, as there's only one such possible conversion. In Rust there are two – static (any) and dynamic (dyn). I think it would be beneficial to make it explicit forever. Disallowing bare Trait as type also helps to teach a beginner that "trait is not a type", which people sometimes incorrectly assume.

@aturon

This comment has been minimized.

Copy link
Member

aturon commented Mar 15, 2017

@withoutboats

One thing not addressed in the RFC is impl Trait in higher order function argument position, that is:

fn some_func(f: impl Fn(impl Debug) -> String)

We've talked about wanting this case (and only this case) to have an existential or possibly higher-rank semantics, rather than universal - a sort of contravariance. Do you think that's not a good idea anymore, or is it just missing from the RFC?

To be clear, this situation is discussed in the RFC:

However, this RFC also proposes to disallow use of impl Trait within Fn
trait sugar or higher-ranked bounds, i.e. to disallow examples like the following:

fn foo(f: impl Fn(impl SomeTrait) -> impl OtherTrait)
fn bar() -> (impl Fn(impl SomeTrait) -> impl OtherTrait)

While we will eventually want to allow such uses, it's likely that we'll want to
introduce nested universal quantifications (i.e., higher-ranked bounds) in at
least some cases; we don't yet have the ability to do so. We can revisit this
question later on, once higher-ranked bounds have gained full expressiveness.

So basically, I still hold the view we discussed back at Rust Belt Rust, but wanted to leave it to a future RFC after we have more experience with full higher-ranked bounds (i.e. ones over types).

@tomaka

This comment has been minimized.

Copy link

tomaka commented Mar 15, 2017

In addition to my previous question, what about this:

trait Foo {
    fn foo(&self) -> impl Trait where Self: Sized;
}

Does that make the "virtual associated type" disabled for &Foo/Box<Foo>?

@aturon

This comment has been minimized.

Copy link
Member

aturon commented Mar 15, 2017

@tomaka You're right that the implications for trait objects are under-specified here. I'll give it some deeper thought soon, but we should definitely strive to make the feature as compatible with trait objects as we can.

@tomaka

This comment has been minimized.

Copy link

tomaka commented Mar 15, 2017

Being able to disable some associated types when you use the trait as a trait object is definitely something I would love to have, but it's probably out of scope of this RFC.

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented Mar 15, 2017

I think that @tomaka's point (about object types) and @cramertj's point (about compatibility with existing traits) are really the same point. I am inclined to agree with @cramertj that it might be nice to have the trait make the associated type explicit:

trait Foo {
    type Output: Debug;

    fn blah(&self) -> Self::Output;
}

while allowing the impl to use impl Trait:

impl Foo for Blah {
    fn blah(&self) -> impl Debug { ... }
}

This would dovetail nicely with some kind of way to infer associated types based on the definitions of other items, which has long been requested (but possibly interacts with defaults). That seems like a mildly complex feature on its own though that merits some amount of thought (basically laying out the precise rules for when such inference occurs).

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented Mar 15, 2017

@krdln

I think [the impact on teaching] is actually the most important question that should matter when deciding on any/some vs. impl.

I agree it is very important that we consider how understandable Rust will be with this feature.

I won't hide I'm now a fan of this syntax (despite being sceptical before) – one nice thing about it is that it's self-explanatory – with impl you have to teach that a function can accept any implementation of a trait and return some instance of a trait. any+some explains this directly in the code itself.

I do not however fully agree with this point. I don't claim it's an open-and-shut case, but I think that any and some have some potential disadvantages when it comes to teaching. For one thing, it effectively requires you to address distinction up front, rather than first giving some examples (where things work as people expect, by and large) to build up intuition, and then coming back (with the explicit syntax in hand) and showing how to "desugar". I usually find that to be the best way to teach, honestly, and hence I'm not sure that having any vs some be distinguished right from the start would be useful.

I do think the choice of some has a lot of potential to confuse people as well -- in particular, the Option<T> type is a new thing that people are learning when they learn Rust, and I think it will be confusing to have Some(x) be something completely unrelated to some Iterator. So even if we opted for two keywords, I'd want two different keywords.

Also, I feel like the "plain English" intution isn't that strong for any/some. i.e., I could see someone describing fn count<T: Iterator>(t: T) -> usize by saying "count takes some iterator and iterates over its items, returning the number of items it contains". Similarly, I feel like fn foo() -> any Iterator doesn't really get at the heart of the distinction. The distinction is really about who chooses the Iterator type that is returned -- the caller, or the callee? e.g., if I return any Iterator, is it clear that I am saying "I will return any iterator you, the caller, want" versus "I will return an iterator that you, the caller, must treat like any other iterator"?

I guess at the end of the day I feel like what is most important for teaching is the existence of an explicit syntax. That is, I think it's important to be able to "desugar" to a syntax that makes the universal/existential distinction explicit, but I don't think that the shorthand syntax has to embody that distinction.

@krdln

This comment has been minimized.

Copy link
Contributor

krdln commented Mar 15, 2017

@nikomatsakis

For one thing, it effectively requires you to address distinction up front

I consider this a good thing, because impl in argument and return position are two totally different features. Using the same syntax for both, while simpler, could be confusing. I may be wrong here, because impl just "does what I mean" and gets out of way, but I think that if I was learning the language, I'd prefer to know that these are separate features from the very beginning and different syntax would help me achieve that. As an example, currently in Rust we use unsafe keyword for both unsafe and trust-me. While unambigous syntactically, it causes some misunderstanding. I'm not saying that this is anyhow related to the "impl vs. two keywords" here, just wanted to point out that using the same word for different concepts might have disadvantages.

Similarity to Some didn't occur to me, I've become too case-sensitive I guess. It may be a valid point, although the some X syntax is quite different from Some(X) and also, they live in different namespaces.

Also, I feel like the "plain English" intution isn't that strong for any/some

Now I see that. So there are basically two issues: (1) You can usually use some instead any in English (I somehow wasn't conciously aware of that, treating some more like a synonym for particular/certain) and (2) It all depends on who reads the signature. I was thinking about the signature as being presented to the caller (or a user reading documentation). But for the callee, the roles are indeed reversed – the function invocation takes some iterator and may return any iterator.

But still, I think these concepts may be worth differentiating syntactically. If not with these keywords, then maybe others. Although I still think that some+any are not that bad – first, Rust is not English, and these are just keywords, second, I think that after a few examples the user should get an intuitive view on what each one of them mean and that the signatures are for the caller.

@steveklabnik

This comment has been minimized.

Copy link
Member

steveklabnik commented Mar 15, 2017

I think that if I was learning the language, I'd prefer to know that these are separate features from the very beginning and different syntax would help me achieve that.

One hard part of teaching is relevance, that is, while many people do like knowing all of the details up front, many people only want to know whatever is relevant for what they're trying to do. To me, this is why the same syntax is important; I don't think many Rust programmers actually care about the difference here. We get people in #rust and #rust-beginners all the time with "why doesn't fn foo<X>() -> X { work?"

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented Mar 15, 2017

@krdln

I consider this a good thing, because impl in argument and return position are two totally different features.

I agree there is a distinction, but I think saying they are totally different is a stretch. There is a deep connection between "universal" and "existential" quantification -- it comes down to "who knows what the value is, and who doesn't". Put another way, no matter where it appears, an impl Trait type indicates a type that -- in some cases -- is not known and where you must work with it via a generic interface (Trait).

In argument position, the caller knows the real type, and the callee has to use the trait. In a very real sense, the hidden type in question is an input to the callee (just like the arguments).

In return position, the callee knows the real type, and the caller has to use the trait. In a very real sense, the hidden type in question is an output from the callee (just like the return value).

These don't seem so wildly different to me, which I think is why in OO-languages it feels so natural to have void foo(Iterator x) and Iterator foo().

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented Mar 15, 2017

I was thinking about the decision to disallow impl Fn(impl Trait) in terms of my previous comment. There I talked about impl trait appearing in arguments versus return -- I think why impl Fn(impl Trait) (and the same with impl Foo<impl Bar>) is a bit risky is that, in the desugared variety, these types do not appear in either argument or return position, but rather in the where-clauses. For example:

fn foo<T, U>(x: T)
    where T: Foo<U>, U: Bar,

Trait matching doesn't have a natural variance (we make all trait matching invariant, for one thing), whereas arguments/return-values do.

This makes me wonder if it makes sense to disallow "nested impl Trait" altogether for now (or maybe this is what the RFC text already said? I remember it as only disallow impl Fn(impl Trait), but I may be wrong). Regardless, it seems wise.

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Mar 15, 2017

To be clear, this situation is discussed in the RFC:

Thanks, I embarrassingly missed this. Leaving it disallowed seems fine to me!

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Mar 15, 2017

it might be nice to have the trait make the associated type explicit:

The only downside of this is that it doesn't work nicely with impl Traits capturing the type params in scope because associated types don't inherently. That is, this could not be impl Trait'd:

trait Foo {
    type Bar;
    fn baz<T>(&self, arg: T) -> Self::Bar;
}
@nikomatsakis

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented Mar 15, 2017

@withoutboats

The only downside of this is that it doesn't work nicely with impl Traits capturing the type params in scope because associated types don't inherently. That is, this could not be impl Trait'd:

Yes, a very good point. And of course extending to ATC quickly gets into higher-order unification / pattern-matching (which of course is also true of the explicit syntax). Seems like it really makes sense to dig more into those algorithms and try to understand the true limits.

correct choice of `some` or `any` seems like an unnecessary burden, especially
if the choice is almost always dictated by the position.
Pedagogically, if we have an explicit syntax, we retain the option of

This comment has been minimized.

@sgrif

sgrif Mar 15, 2017

Contributor

Is it possible to use a more accessible word here?

```rust
trait Iterable {
type Item;
fn iter<'a>(&'a self) -> impl Iterator<&'a Item> + 'a

This comment has been minimized.

@sgrif

sgrif Mar 15, 2017

Contributor

This should be Self::Item not Item

@sgrif

This comment has been minimized.

Copy link
Contributor

sgrif commented Mar 15, 2017

One thing that's unclear to me from the wording of this RFC is whether the signature fn foo<'a>(&'a self) -> impl Iterator implies that the return type is 'a or 'static. Early in the RFC it seems to say that it would be 'static, but when it gets into the lifetimes of type parameters which are in scope it gets foggier to me. For what it's worth, I've found the fact that fn foo<'a>(&'a self) -> Box<Iterator> implying 'static instead of 'a to be one of the most unintuitive and confusing things about Rust as a language, especially considering that the compiler guides you towards sticking a bunch of : 'static bounds on types and not specifying the lifetime of the return type, which is the right thing to do. I think applying the same lifetime elision rules that apply for normal types would be consistent and the least confusing thing to do here.

I'm also curious how this could apply to something like Diesel where we have two "phases" of traits that are usually implemented. The first set applies to query construction, and handles things like type checking. The second set applies to actually executing the query, and making sure that it doesn't contain types or expressions specific to another backend. We could theoretically end up with a trait that looks like this:

trait QueryDsl {
    fn select<T>(self, selection: T) -> impl QueryDsl;
    fn filter<T>(self, predicate: T) -> impl QueryDsl;

    fn execute<Conn>(self, conn: &Conn) -> QueryResult<usize> where
        Conn: Connection,
        Self: QueryFragment<Conn::Backend>;
}

However, I suspect that would mean that this code would not work:

let conn = PgConnection::establish("...").unwrap();
users.select(id).execute(&conn).unwrap()

Since that would knowing that the return type of users.select(id) implements QueryFragment<Pg>. Am I correct in that assumption? Is there any way to work around this in the system proposed by this RFC? Even though ATCs eventually service this, it'd be nice to be able to avoid some of the long and scary where clauses that would still have to appear.

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Mar 15, 2017

re lifetimes: My understanding is that the proposal is to work exactly like trait objects today. impl Trait = impl Trait + 'static, but you can instead make it impl Trait + 'a.

@Ericson2314

This comment has been minimized.

Copy link
Contributor

Ericson2314 commented Mar 15, 2017

I'm quite pleased with the fully explicit strawman :). Things like that make me less worried than I'd be otherwise.

@golddranks

This comment has been minimized.

Copy link

golddranks commented May 26, 2017

I wonder if the planned SemVer API checking tool could catch the implicit changes in the API W.R.T Send and Sync.

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented May 26, 2017

The above comments are overlooking the fact that Send and Sync are merely two examples of "auto traits" and while the mechanism is not in stable yet, other crates can create their own.

So there is no real way to detect when a breakage from that could happen, without a crates.io list of "auto traits" people might use, and what their rules are, and even that is an approximation.

@daboross

This comment has been minimized.

Copy link

daboross commented May 26, 2017

@eddyb

Even with Send and Sync being the only two, it this not still a large concern?

Personally I think it's fine as long as nothing is inferred from the function body, and it is all based on type signatures that would be a breaking change to change anyways. Even with just the signature, and even having just two implicit traits, I could see this being a very confusing implied relationship.

Kind of like how #[derive(Clone)] auto-inserts where T: Clone for all type parameters - but with impl Trait, there's no "manual implementation": you're stuck with the sometimes-slightly-off assumption the compiler makes?

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented May 26, 2017

Send + Sync inherently introduce breakages because of their inferred nature. This is already true. The word Send might not appear anywhere in your crate, but if you add an Rc field to a type, that's a breaking change. This exactly the behavior this RFC establishes, except applied to struct definitions instead of functions.

We have rules that adding impls is not a breaking change, but this does not apply to impls of auto traits (positive or negative). The rules about what these impls mean are subtle and actually rather surprising.

In other words, what the RFC has proposed is consistent with decision we have already made about auto traits: in order that types be Send and Sync by default, so that users are never bashed against the forgetfulness of a library author regarding them, they introduce the opportunity for breaking changes.

while the mechanism is not in stable yet, other crates can create their own.

Well I'd strongly prefer never to stabilize the ability to define auto traits & to treat it essentially as a kind of lang item. Precisely because the concerns people are raising already apply in other contexts. Auto traits violate all of our rules.

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented May 26, 2017

Well I'd strongly prefer never to stabilize the ability to define auto traits & to treat it essentially as a kind of lang item.

We've been moving for years in the other direction (they used to be called "type kinds").

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented May 26, 2017

That's not how I would interpret what's happened to Send and Sync. I'd say that they've become more like normal traits - that doesn't imply its a good idea to make more traits that can be abnormal in the way that they still are. From my perspective the motion has been toward not having auto traits at all (though it stopped short of that obviously).

@petrochenkov

This comment has been minimized.

Copy link
Contributor

petrochenkov commented May 29, 2017

This RFC talks mostly about functions, but constants may want both kinds of impl Trait - "some" and "any".

// Generic integer constants
const C1: impl __Integer__ = 10; // const C1<T: __Integer__>: T = 10; // "any"

// Closure constants
const C2: impl Fn() = || {}; // const C2: __UniqueClosureType__ = || {}; // "some"

What the "post-rigorous" way to discern between them would be?

@daboross

This comment has been minimized.

Copy link

daboross commented May 30, 2017

@petrochenkov I believe const integers being any intereger will probably be a completely separate proposal, especially because it'd have to decide on what the 'any integer' type is. I'm not sure that would even want to use impl Trait, since as of this proposal, impl Trait represents only represents "some" in an "output" position.

Edit (now that I'm on desktop):

To clarify, I mean in const X: T = Y;, all of the const variable is "output", much like the return type from a function. I believe the reasoning for one impl Trait syntax was to have one syntax which is:

  • Some type for all "output" types (like returning from a function)
  • Any type for all "input" types (like function arguments)

Since const variables arguably fall into the former category, I would argument const C1: impl __Integer__ deserves some other syntax, and should not use impl Trait.

@scottmcm

This comment has been minimized.

Copy link
Member

scottmcm commented May 30, 2017

My mental model of const is that it's "a const fn that's auto-called", which would argue for the latter ("some") interpretation.

@petrochenkov

This comment has been minimized.

Copy link
Contributor

petrochenkov commented May 30, 2017

@daboross

as of this proposal, impl Trait represents only represents "some" in an "output" position.

I've rechecked, the RFC as merged introduces impl Trait in argument position as "any", and const C1: impl __Integer__ is equivalent to it.

@scottmcm

My mental model of const is that it's "a const fn that's auto-called", which would argue for the latter ("some") interpretation.

I agree with this interpretation, but if it's followed, then the generic constant case (which is practically more important, IMO) is left without sugar, unless some new "some"/"any" separation is reintroduced, which this RFC tried to avoid.

@torkleyy

This comment has been minimized.

Copy link

torkleyy commented Jun 7, 2017

The RFC is already merged, but is there any chance we could rethink the decision to introduce impl Trait in argument position?

I'd like to respond to some of the arguments provided in the RFC to explain why I dislike the idea of having impl Trait in argument position:

Argument from learnability

Now, consider a new Rust programmer, who has learned about generics:

fn take_iter<T: Iterator>(t: T)

What happens when they want to return an unstated iterator instead? It's pretty natural to reach for:

fn give_iter<T: Iterator>() -> T

I'm not sure about this. I actually don't see a reason why somebody would do that, because you don't want it to work with every type implementing Iterator. Also, @steveklabnik explains this very clear in the book:

Sometimes, when writing a function or data type, we may want it to work for multiple types of arguments. In Rust, we can do this with generics.

from TRPL, chapter about generics

So it's also clearly about working with "multiple types". Using impl Trait as return type, however, is clearly a way of letting the compiler infer the actual type, but providing certain guarantees which traits it implements. If I were to see impl Trait in argument position without knowing about these plans, I'd actually think it's some clever type inference by the compiler, not generic over types implementing that trait.

Now, let's assume it actually would make it a bit easier for a beginner.

  1. While it's very important to be easy to learn, I'd say that impl Trait in argument position is more of a response to a beginners misunderstanding than a good feature in the language. I don't think avoiding one mistake of a beginner is a reason to have a "half-broken" language feature (please excuse the description as "half-broken", I don't know how to describe it more appropriately).

  2. It essentially allows the beginner to proceed with a wrong mental model (and that their code works approves this model).

Argument from ergonomics

This one is basically about

fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U>

being more ergonomic than

fn map<U>(self, f: impl FnOnce(T) -> U) -> Option<U>

First of all, I feel this is a rather weak argument. While I agree that you get the meaning of f in the latter one a tad faster, I don't agree that it's worth having this feature.

Another point I'd like to raise here is that there is no comparison to using a where-clause:

fn map<U, F>(self, f: F) -> Option<U>
    where F: FnOnce(T) -> U
fn map<U>(self, f: impl FnOnce(T) -> U) -> Option<U>

It might be highly subjective, but I find the one with where actually the most easy one to read. I'd like to hear other opinions on this, probably somebody else doesn't feel this way.

Argument from familiarity

In Java, non-static methods aren't parametric; generics are used at the type level, and you just use interfaces at the method level.

I don't agree here. Consider the following:

trait Foo {}

fn foo(v: Vec<impl Foo>)

vs

interface Foo {}

void foo(ArrayList<Foo> list)

While the Rust example expects a Vec containing elements of some type implementing Foo (so all elements being of the same type), the Java example accepts a heterogeneous collection.

I don't know how to reply to the other part of the argument, because I'm not sure how it is related to impl Trait.

Stylistic overhead

As mentioned in the RFC, there was a point raised by @nrc about it being stylistic overhead / one more syntax to learn.

Now, this is a rather weak argument on my side, but

  1. it's true that it's one more syntax, although the learning effort is very small. I agree with your argument that it's not a new concept.
  2. [..] since expanding out an argument list into multiple lines tends to be preferable to expanding out a where clause to multiple lines [..]

I personally prefer the where syntax here again (it seems the RFC doesn't use the rustfmt formatting here), but again, this is just a matter of taste, so I wouldn't see any of these arguments valid to make the decision.


In the end, I'd like to rephrase your words that "it's not a new concept" but rather a new syntax for an existing concept. So it essentially boils down to the question whether or not the syntax is better and whether or not it's good to have 3 alternative syntaxes.

For me, that's a clear no, given that I find it more confusing from the perspectives I explained above, namely

  • misleading familiarity from other languages,
  • the approval of a wrong mental model and
  • the semantic difference of impl Trait in argument position and as return type.

@aturon Please don't take this personal, you're doing great work! I just wanted to give my opinion here, as a Rust library developer. Also note that I don't know very much about type theory, so I could be missing substantial facts which invalidate some of my arguments; if that's the case, please tell me and I'll remove them.

Thanks for reading!

@Storyyeller

This comment has been minimized.

Copy link

Storyyeller commented Jun 7, 2017

One other thing I find weird about the impl Trait in argument position syntax is the type parameters that are used without being declared anywhere. I think that would lead to confusion for beginners as well.

@RalfJung

This comment has been minimized.

Copy link
Member

RalfJung commented Jun 7, 2017

So it's also clearly about working with "multiple types". Using impl Trait as return type, however, is clearly a way of letting the compiler infer the actual type, but providing certain guarantees which traits it implements. If I were to see impl Trait in argument position without knowing about these plans, I'd actually think it's some clever type inference by the compiler, not generic over types implementing that trait.

I think there's some misunderstanding here: impl Trait is not about inference. The following are not equivalent:

fn fn1() -> impl Debug { vec![0] }
fn fn2() -> Vec<i32> { vec![0] }

The first hides the fact that we return a vector from the outside world, and instead just exposes the fact that "something that implements Debug" is returned. There is abstraction going on here and that's an important part of this feature.

In the following code, the function body doesn't know anything about the type of v other than that it implements Debug:

fn v_consumer(v: impl Debug) { ... }

This is exactly the same way in which a function using the return value of fn1 does not know anything about the type that it got back.
(Somewhere deep down, this is because there is a duality between universal and existential quantifiers.)

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Jun 8, 2017

@torkleyy Thanks for giving this feedback about this feature - as the thumbs show, there are users who share your viewpoint. However, I want to deal with this RFC from a project-procedural perspective for the moment.

The arguments you raise are not novel; in other words, they're all perspectives that we've considered before. Since the RFC has already been merged, and these arguments were made during the period before it was merged, I really don't see any reason we would revert that action now.

However, merging an RFC doesn't mean the feature will become stable - all it means is that it will become available under a feature flag on nightly. Many features which were RFC merged have languished in instability or were changed significantly for one reason or another. It remains possible that your viewpoint will prevail.

But we cannot constantly relegislate the same arguments again and again; if we did, whatever happens to be the status quo would always win, and the language will never make progress. Instead what really matters now is the new insights we gain from using the feature in real Rust code.

What I want to encourage you to do, then, is wait until impl Trait in argument position can be used on nightly & then attempt to use it critically, and provide user feedback based on that experience. That feedback, from people with different perspectives, is what's most valuable when trying to make decisions in the phase we're in now.

@torkleyy

This comment has been minimized.

Copy link

torkleyy commented Jun 8, 2017

@withoutboats Thanks for the fast response.

we cannot constantly relegislate the same arguments again and again

I see.

What I want to encourage you to do, then, is wait until impl Trait in argument position can be used on nightly & then attempt to use it critically, and provide user feedback based on that experience. That feedback, from people with different perspectives, is what's most valuable when trying to make decisions in the phase we're in now.

Makes sense, I'll do that. Thank you again for explaining.

@fstirlitz

This comment has been minimized.

Copy link

fstirlitz commented Mar 31, 2018

(Somewhere deep down, this is because there is a duality between universal and existential quantifiers.)

Namely, it corresponds to the theorem of first-order logic (and of dependent type theory) that ∀ x: (P(x) → Q) if and only if (∃ x: P(x)) → Q. If you expand p → q as ¬p ∨ q, you can consider it a corollary of a form of de Morgan duality (∀ x: ¬P(x) iff ¬∃ x: P(x)) and distributivity of irrelevant quantifiers; if you set Q := ⊥ and ¬p := p → ⊥, you can consider it a generalised de Morgan duality.

@RalfJung

This comment has been minimized.

Copy link
Member

RalfJung commented Mar 31, 2018

Fortunately, this duality between universal and existential quantifier holds even in fully constructive higher-order logics, no need to equate P -> Q and ~P \/ Q :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment