Skip to content
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

Add RFC undo-universal-impl-trait. #2444

Closed

Conversation

hadronized
Copy link
Contributor

@hadronized hadronized commented May 21, 2018

Rendered


Closed and locked; see comment from leadership below.

@roblabla
Copy link

Aren't universal impl trait stable since 1.26 ? This would be a breaking change.

@CryZe
Copy link

CryZe commented May 21, 2018

Aren't universal impl trait stable since 1.26 ? This would be a breaking change.

I guess you could remove it as part of Rust 2018. That should work

@GuillaumeGomez
Copy link
Member

I like the idea.

@hadronized
Copy link
Contributor Author

Aren't universal impl trait stable since 1.26 ? This would be a breaking change.

It would be breaking change.

@gmorenz
Copy link

gmorenz commented May 21, 2018

Also, consider this function that takes an argument and add it to itself:

fn add_self<T>(x: T) -> T where T: Add<Output = T> + Copy

Now consider:

fn add_self(x: impl Add<Output = impl Copy> + Copy)

You can see the duplication here and the overwhelming confusion that both the impl Trait will
resolve to the same type, even though they don’t have the same contract, which is impossible to
guess from the interface while it could. This is legal because we only talk about two contracts here
and the function will pick a type at the union (it must be Add + Copy and Copy) but you also
have that weird Output contract as well.

Even weirder:

fn add_self(x: impl Add<Output = impl Sized> + Copy) -> impl Sized {
 x + x
}

The impl trait versions here are better code, since they give a better specification of what the function actually does. The equivalent non impl trait version to the best impl trait version would be

fn add_self<T: Add<Output = O> + Copy, O: Sized>(x: T) -> impl Sized {
  x + x
}

We can make a non-impl trait version that is even better, since we can specify that the Add::Output type is equal to the return type of the function

fn add_self<T: Add<Output = O> + Copy, O>(x: T) -> O {
  x + x
}

We can mix this with impl trait for what is my preferred version

fn add_self<O>(x: impl Add<Output=O> + Copy) -> O {
  x + x
}

What I get from this whole line of reasoning though is that universal impl trait encourages better code. It makes you realize what types need to be equal, and what ones don't.

@Boscop
Copy link

Boscop commented May 21, 2018

I'm very much in favor of removing impl Trait for arg types:

  1. impl Trait for arg types is non-orthogonal, which makes it harder to decide which way of universal quantification to use, and this question will come up over and over and over just because there are now 2 ways.
  2. It's not actually easier to learn than generics. It looks very different from generics syntaxes that most programmers are familiar with.
  3. It's easier for learning Rust to NOT conflate universal and existential quantification.
    People should be aware of the difference, so that's another argument to keep only <T: Trait> for arg types and impl Trait only for existential quantification (including for other cases of existential quantification later when impl Trait will be allowed for const/let and existential types).
  4. The argument that "having impl Trait for arg types allows postponing introducing generics until later in the book" doesn't count, because newbies will have to read the Rust book multiple times anyway.
  5. Most newbies will be familiar with another language that has similar generics syntax, e.g. C++, C#, Java, D, Scala etc.
  6. There is NO evidence that impl Trait makes generics/Rust easier to learn, quite the contrary, it seems to confuse people, and:
  7. From personal experience teaching Rust to complete newbies (no previous programming experience), and to some who only knew C or Python:
    Universally quantifying generics are not a difficult part of Rust (but references/lifetimes in generics).
    And anyone coming from any other language with universally quantifying generics will quickly grasp the Rust syntax, but impl Trait is completely foreign to them.
  8. The argument that impl Trait for arg types should be introduced to mirror impl Trait for return types is invalid: We Rust programmers lived without impl Trait for return types for years, it can be postponed until the end of the book, until newbies have understood universally quantifying generics! Newbies can write a lot of code before ever even needing impl Trait for return types.
  9. There is no reason to ever choose to write impl Trait for arg types as a non-newbie, so why should newbies learn this way, if they have to unlearn it very quickly anyway?
  10. I disagree with the dialectical ratchet in this case.
    It may apply to references/lifetimes but NOT to impl Trait for arg types.
    The concepts of references and lifetimes complement each other, they are both necessary concepts that are used together, not just by newbies. It's no detour for newbies to learn them because they are the right way to do things already because there is only one way, because these are orthogonal concepts!
    But having "impl Trait for args" as "the newbie-way to express universally quantified generics" means that newbies have to learn something that they have to unlearn later!
    Which WILL confuse them because there are 2 overlapping ways to do the same thing and the one they learned is stricly less powerful, which demotivates them because they now have to unlearn it and learn the fully powerful way! And then they will ask themselves: "why did i even have to do the detour through "impl Trait for args", this <T: Trait> syntax is more familiar and more powerful"..
  11. We should not compromise orthogonality of the language to introduce a strictly less powerful syntactic sugar, ESPECIALLY because it will only confuse people more.
  12. The Rust language contains a lot of concepts, everyone will have to read the book a couple times. Making Rust easy to use and easy to remember is at least as important as making it easy to learn, and the bottleneck for learnability is the docs, not the language design itself. The docs about generics can always be improved without affecting Rust's design, we shouldn't compromise Rust for this!
  13. What makes a language easy to use and remember is the consistency and orthogonality of the concepts, so that there aren't overlapping concepts, and so that in every situation there is one obvious way to do things. Not 2 ways, where one is strictly less powerful. (It's like having traits + OOP inheritance, makes it harder to choose which one to use.)
  14. In the future, we should really do some A/B testing to determine which change to Rust makes it actually easier to learn, before stabilizing that change. And when features are introduced that are controversial or unorthogonal, the debate thread should be more visible and open for longer. I only noticed that impl Trait was also being stabilized for args when it was too late to comment on it, because it was kind of hiding in the shadow of impl Trait for return types.

(Sorry if this sounds like a rant, I just feel very strongly about this :)

@Restioson
Copy link

Restioson commented May 21, 2018

I completely agree with this PR. I didn't understand why we had two superfluous syntaxes for the same thing (although impl Trait is less useful as you can't turbofish it). It's also really really confusing to use the same syntax for two subtly (and completely) different bounds -- already some (I used to be until I did some reading) may be confused as to what the differences of universal and existential quantification are, thus making them share syntax is even worse for clarity.

@logannc
Copy link

logannc commented May 21, 2018

Does anybody actually think in terms of 'caller' vs 'callee' chosen types? Are we really confusing people aware of the type theory involved for more than a few moments?

impl Trait is an anonymized type known to the compiler to implement Trait. With that definition, the distinction between 'universal' vs. 'existential' goes away and, for symmetry, it makes sense to have it available in both argument and return positions.

@vorot93
Copy link

vorot93 commented May 21, 2018

What we see here is a huge failure of public review process. Simply put, universal-impl-trait escaped attention by latching onto conservative-impl-trait PR. We all waited and prepared for the latter and thus didn't notice this obscure feature before too late.

@Restioson
Copy link

Restioson commented May 21, 2018

@logannc Yes, we are confusing people who understand what the difference between universal impl Trait and existential impl Trait. I researched the topic extensively to understand it, and after a few hours I was still sort of uneasy as to why we had it do two perpendicular things.

impl Trait is an anonymized type known to the compiler to implement Trait. With that definition, the distinction between 'universal' vs. 'existential' goes away

No, this distinction does not go away. As a universal bound there are multiple types you can call the method on (i.e fn x(a: impl T)), but as an existential bound there is exactly one (i.e fn x() -> impl T). This makes it different for monomorphisation and for normal programming too -- you could expect fn x() -> impl Iterator<Item=u32> to return any given type that implements Iterator<Item=u32> (similar to how collect returns any type implementing FromIterator).

Copy link
Contributor

@sgrif sgrif left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This RFC strikes me as in bad faith. This feature had an RFC open for 2 months before it was merged. It's been available on nightly for many more months than that. This mostly seems like a rehashing of arguments that were already made and considered during that time period.

While we should certainly be allowed to undo things that turn out to be mistakes, I don't think it's in the spirit of this community to be opening PRs to undo features you dislike within weeks of their stabilization.

The biggest question I'd ask is what's changed since the RFC to justify this? The majority of this seems to be "I saw the rationale behind this feature and I disagree", without presenting any new information that would change the decision.

`impl Trait` carries its own semantics and because anonymization in return type is not a trivial
feature people will use without actually wanting it.

Also, people are used to generics. People coming from C++, C#, Java etc. actually know about
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've followed

This argument is twisted along the lines of subjective opinion about learnability

With an equally subjective argument.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

People come to Rust from languages other than C++, C#, and Java. Not to mention that the entire point of impl Trait in argument position is that it works much closer to Java/C#. In java, you don't write void <T extends SomeInterface> method(T arg), you write void method(SomeInterface arg).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interfaces in Java are not akin to universal quantification, because those are dynamic dispatched variables. The equivalent in Rust would be &Trait or dyn Trait.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, but the entire point is that you generally don't care about the tradeoffs between a monomorphised function and a dynamically dispatched one. Similarly, you generally don't need to care about the differences between returning Box<Foo> and impl Foo. This is a "do what I mean" feature. Similar to lifetime elision, it hides details that you usually don't care about. If you end up in a situation where you do need to care about it (T: Add<Output = T> is a good example), or want to care about it (you're in a situation where binary size is very important), then you learn the underlying mechanisms.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You generally

Are you sure about that? :)

Copy link

@Boscop Boscop May 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sgrif
Wouldn't the actual equivalent of

void foo(SomeInterface arg)

be

fn foo(arg: Rc<RefCell<dyn SomeTrait>>)

?

Because the instance is GC'ed and mutable.

So when translating Java code (or thinking) to Rust, one wouldn't just be able to write fn foo(arg: impl SomeTrait) instead of the java method anyway, because that would be different semantics (it takes ownership and can't mutate). One would have to re-architect the code in the translation process to use references/lifetimes and to please the borrow checker anyway.
So impl Trait for args isn't really the equivalent (or what one wants) in most cases when translating Java/C# code.

way to use generics (`impl Trait` in argument position is a use of hidden generics, that is,
anonymized type variables with explicit trait bounds). For people who don’t come from such
languages or for completely newcomers, we are assuming that generics are too hard to understand at
first but people will eventually need to learn them, as `impl Trait` in argument position is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no different than lifetime elision. fn foo(&self) is strictly less powerful than fn foo<'a>(&'a self).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How so?

Copy link

@gmorenz gmorenz May 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fn foo(&self, x: &str) -> &str vs fn foo<'a, 'b>(&'a self, x: &'b str) -> &'b str. You can use the second in more places, e.g. with 'a=lifetime of some object and 'b = 'static (or just the lifetime of some longer lived object).

This comes up in real code on (rare) occasion.

fn foo<T>(a: T, b: T, c: impl Debug) where T: Add<Output = T> + Copy
```

This function has three type variables to substitute to be completely monomorphized. However, it’s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you meant two type variables

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the number of variables being monomorphised something people frequently care about? Is it a case we need to optimize for over readability?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, thanks for noticing. For the number of variables, I actually care about that, because it gives me hints about code generation and which variables I can have control over – via turbofishing, for instances. It’s way harder with impl Trait because you’re not allowed to turbofish anymore. 😞

Now consider:

```
fn add_self(x: impl Add<Output = impl Copy> + Copy)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This signature is not equivalent. The only output type you could have here is impl Copy or ()

fn add_self(x: impl Add<Output = impl Copy> + Copy)
```

You can see the duplication here and the overwhelming confusion that both the `impl Trait` will
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This argument is entirely subjective. I personally don't find this confusing. I'm taking "some type that can be added and copied. The result of the addition is some type that can be copied".

"overwhelming confusion" feels like unnecessary rhetoric to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re right, it’s too much subjective. I’m removing it.


### Summary of arguments

This document has shown that `impl Trait` in argument position is confusing for newcomers and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This document hasn't really shown anything from newcomers. It's pretty hard for us to gauge how this affects newcomers at this point, since it does not appear in the book or anywhere in the documentation.

Instead this document seems to be claiming every other argument is subjective, but this one is somehow objective.

Perhaps the source of confusion is the fact that it's a brand new, undocumented feature that people are still just starting to use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve linked a few links in the top of the document. Also, reactions and the fact it’s been so opinionated is somehow a hint that there’s a massive confusion, right?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, personally I was confused as to why we had the same syntax for the same thing, but that's just me I guess.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s not just you. All of this is very confusing and opinionated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, reactions and the fact it’s been so opinionated is somehow a hint that there’s a massive confusion, right?

People having strong opinions does not mean they're confused.


## Why is `impl Trait` in argument position wrong?

People out there didn’t really realize what was going on. Complains and confusions on IRC, reddit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a straw man to me. The feature is not yet documented. There is always going to be confusion until that gets remedied. There's also a lot of confusion around how reborrowing works (another undocumented feature), and that feature's been around for much longer.

because they will maintain and contribute to codebases they haven’t read nor written code for
before.
- Turbofishing is impossible with `impl Trait`, forcing you to use type ascription in other
places.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fixable.
Technically, nothing prevents supporting turbofish and making arg position impl Trait purely a sugar for named type parameters.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's slightly more arcane, though. Often when doing something like fn x<A: TraitA, B: TraitB<A>>(b: B) I write it with the thing the 'main' generic depends on first (I don't know if you are allowed to put it second, but either way it feels 'wrong' to do it in reverse). This might not be so with impl Trait turbofishing, or at any rate is more difficult to figure out.

@xgalaxy
Copy link

xgalaxy commented May 21, 2018

Excuse the stupid question.

How is impl Trait in argument position any different than say.. C# where a function or method takes an interface instead of a concrete type? Can someone explain that to me. Because in C# I don't feel like its confusing at all. I'm still learning Rust but when I saw impl Trait in argument position I just equated it to doing the same thing I would do in C# with interfaces.

@hadronized
Copy link
Contributor Author

I added a bunch of comments from here. Thank you all for such massive participation.

@sgrif
Copy link
Contributor

sgrif commented May 21, 2018

@xgalaxy

How is impl Trait in argument position any different than say.. C# where a function or method takes an interface instead of a concrete type?

In terms of program behavior it's equivalent to passing an interface in C#. Arguably the closer equivalent would be taking a "trait object", but this is something that only matters if you care about very specific performance trade offs that you don't normally need to care about.

I'm still learning Rust but when I saw impl Trait in argument position I just equated it to doing the same thing I would do in C# with interfaces.

That was the goal behind the feature.

@hadronized
Copy link
Contributor Author

hadronized commented May 21, 2018

@xgalaxy to me, it’s different because of dynamic dispatch vs. static dispatch. The Rust equivalent would be:

fn foo(x: &Debug)
fn foo(x: dyn Debug) // with the new syntax

Both those functions are completely monomorphic, not polymorphic.

@bowbahdoe
Copy link

Perhaps this is the wrong time and place to ask this, but what is turbofishing and why does it matter that it is impossible with impl Trait?

@hadronized
Copy link
Contributor Author

hadronized commented May 21, 2018

@bowbahdoe turbofishing is a way to force monomorphization of a function by providing it with the type variables substituted. You already have done it for sure. See:

let x = (0..10).collect::<Vec<_>>(); // the syntax ::<_>() is turbofishing

You cannot do it with impl Trait because of the anonymized type variable.

@vorot93
Copy link

vorot93 commented May 21, 2018

@sgrif Are you referring to impl Trait in general or universal-impl-trait in particular. We all discussed conservative-impl-trait, but impl Trait in argument position? Not so much.

And being available in the nightly means nothing. There are a lot of experimental and incomplete features that can be turned on via feature gate and may go away at any time. Just look at non-existant trait aliases for example.

@sgrif
Copy link
Contributor

sgrif commented May 21, 2018

@phaazon There's an important detail that you're leaving out, which is that turbofishing is almost never required for type variables used in an argument position. The compiler can infer it from the type of the argument you're passing. The only time I've ever needed to turbofish an argument parameter is when passing a generic extern "C" function pointer.

In fact, I hope the way this ends up getting resolved is to allow type parameters to be explicitly passed, but no way to explicitly state the type of impl Trait, as it's a strength of the feature. Take for example this function in Diesel:

fn debug_query<DB, T>(query: &T) -> DebugQuery<T, DB>
where
    DB: Backend,
    T: QueryFragment<DB>,

The type of DB can never be inferred. The type of T is always inferred. 100% of the usage of this function looks like debug_query::<Pg, _>(&query). Instead we could write it as:

fn debug_query<DB: Backend>(query: &impl QueryFragment<DB>) -> DebugQuery<impl QueryFragment<DB>, DB>

which would remove the pointless , _ from the caller.

@Pzixel
Copy link

Pzixel commented May 21, 2018

@sgrif correct me if I'm not right, but your code

fn debug_query<DB: Backend>(query: &impl QueryFragment<DB>) -> DebugQuery<impl QueryFragment<DB>, DB>
    T: QueryFragment<DB>,

is equivalent of

fn debug_query<DB, T, U>(query: &T) -> DebugQuery<U, DB>
where
    DB: Backend,
    T: QueryFragment<DB>,
    U: QueryFragment<DB>

Which obviously is not the same thing as

fn debug_query<DB, T>(query: &T) -> DebugQuery<T, DB>
where
    DB: Backend,
    T: QueryFragment<DB>,

So you probably just planted a bug in the oneliner.


As pointed out below, it's a bit incorrect, there is no 3rd type parameter. However, it shows that U is not related to T which may be unwanted.

@Phlosioneer
Copy link
Contributor

Phlosioneer commented May 21, 2018

@logannc I think you're both right and wrong. You're right, it's simply a stand-in for "some type that implements this trait, the compiler will figure out the rest." However, it does not exist in a vacuum. It must interact with the existing type system. And in that, it fails. Badly. Take for example, this function signature:

fn is_sorted(data: impl IntoIterator<Item = u8>) -> bool;

Now, the user wants to make the function more robust by adding a Result type. They want to return the original data, because they're passing it in by value somewhere. How? They might try this:

fn is_sorted(data: impl IntoIterator<Item = u8>) -> Result<bool, impl IntoIterator<item = u8>>

But now this does something different. The compiler forgets the input type. So if the user inputs a Vec and expects the same thing out, they might try to debug it with {:?}. But they will get an error: impl std::iter::IntoIterator cannot be formatted using :?! This is very surprising and not intuitive at all. The same thing would have worked perfectly with rust's normal generics.

Very basic assumptions about how the user expects types to be propagated are lost BECAUSE we have a type system that is strict- the compiler is not going to "figure it out at the call site", it's just going to follow the exact definition in the function signature. There is no way to write this without named generics.

@sgrif and @Pzixel just demonstrated this problem.

@sgrif
Copy link
Contributor

sgrif commented May 21, 2018

Another example from Diesel that would be improved by impl Trait in argument position:

fn transaction<T, E, F>(&self, f: F) -> Result<T, E>
where
    F: FnOnce() -> Result<T, E>,
    E: From<diesel::Error>,

in typical usage, T and F are inferred, and F is a closure (so cannot be named). E however, is usually explicitly given, since the body of the closure will likely use ?. So it's normally invoked as conn.transaction::<_, diesel::Error, _>(|| stuff()?; Ok(()))

This case is a little more interesting, since we can remove the F parameter, but we can't remove the T (unless FnOnce were stabilized in a form where the arguments are an associated type). However, we could change the signature to:

fn transaction<T, E>(&self, f: impl FnOnce() -> Result<T, E>) -> Result<T, E>

which would change the invocation to conn.transaction::<_, diesel::Error>(|| stuff()?; Ok(()))

@sgrif
Copy link
Contributor

sgrif commented May 21, 2018

@Pzixel that is incorrect.

@hadronized
Copy link
Contributor Author

@phaazon There's an important detail that you're leaving out, which is that turbofishing is almost never required for type variables used in an argument position.

That’s worth mentioning, @sgrif. I’ll alter the RFC, thanks for noticing!

@Phlosioneer
Copy link
Contributor

Phlosioneer commented May 21, 2018

@sgrif Yes, but how do you teach that to a newbie? The difference here goes back to the caller vs callee distinction. You need to know about that distinction to be able to understand why those two functions are different.
(For others, here's the correct form:

fn debug_query<DB, T>(query: &T) -> DebugQuery<impl QueryFragment<DB>, DB>
where
    DB: Backend,
    T: QueryFragment<DB>

)

@Pzixel
Copy link

Pzixel commented May 21, 2018

@sgrif

that is incorrect.

Well, could you elaborate a while then, please? IIRC each input impl Trait becomes a generic parameter. If you say that output parameter would be tied to the input one then how compiler could figure out if they are tied? How could I express then that they are different? I do believe that this one shows that I'm right

fn foo(value: &impl std::fmt::Debug) -> impl std::fmt::Debug {
    42
}

fn main() {
    foo(&"42");
}

input type is not tied to the output one in any way.

@sgrif
Copy link
Contributor

sgrif commented May 21, 2018

@Pzixel It's equivalent to fn debug_query<DB, T>(query: &T) -> DebugQuery<opaque type you don't know anything about and don't choose, DB>. Whether or not the return type is the same as the input type is unimportant to this API. If I wanted to explicitly state that they were the same type, I'd give it a name and make that clear (e.g. make it a type parameter)

@Phlosioneer
Copy link
Contributor

@Pzixel only the input impl Traits can be replaced by named type parameters. Output ones are not expressible by the named-type-parameter typesystem, because the function decides their type based on the inputs, rather than the caller of the function declaring a type for U.

In your example:

fn foo(value: &impl std::fmt::Debug) -> impl std::fmt::Debug {
    42
}

fn main() {
    let dbg1 = foo(&"42");
    let dbg2 = foo(Some(3));
    let debugs = vec![dbg1, dbg2];
    println!("all debugs: {:#?}", debugs);
}

This works with type parameters (if you declare the returning type to be u8 or something like that, to resolve the ambiguous integer type). It does NOT work with impl Trait - at no point does the function guarantee it returns the same output type for those two different input types.

@sgrif
Copy link
Contributor

sgrif commented May 21, 2018

@Phlosioneer I think you're over-emphasizing the importance of knowing/caring about callee chosen vs caller chosen. A better comparison to make would be the difference between fn foo(x: &Display) -> &Display and fn foo(x: impl Display) -> impl Display, as they're much closer semantically.

@Boscop
Copy link

Boscop commented May 21, 2018

@sgrif IIRC, in the D language, turbofish allows omitting type params (from the end), they don't have to be substituted by _. If we had this in Rust, you wouldn't need impl Trait for arg types to satisfy your use case :)

So then you'd put all the inferrable type params at the end of the <..> list when defining the function so that a caller can omit them.

@sgrif
Copy link
Contributor

sgrif commented May 21, 2018

@Boscop Yup, that's a direction we could go as well. As with all things, it's a tradeoff.

@Pzixel
Copy link

Pzixel commented May 21, 2018

@sgrif

It's equivalent to fn debug_query<DB, T>(query: &T) -> DebugQuery<opaque type you don't know anything about and don't choose, DB>. Whether or not the return type is the same as the input type is unimportant to this API. If I wanted to explicitly state that they were the same type, I'd give it a name and make that clear (e.g. make it a type parameter)

Yes, you're right, thank you.

However, I still think that it leads to confusion. There should be only exact one way to do some thing.

The main reason against impl Trait in input parameter is that it procides hard-to-spot monoporphisation. As you pointed out, fn foo(x: &Display) -> &Display and fn foo(x: &impl Display) -> impl Display look almost the same, while they are different. Some generic salt is a good here, imho. I can easily spot that fn foo<T: Display(x: &T) -> impl Display will have a static dispatch, and I still can write fn foo<T: Display(x: &T) -> T if I want to express relationship between input and output types.

@sgrif
Copy link
Contributor

sgrif commented May 21, 2018

However, I still think that it leads to confusion. There should be only exact one way to do some thing.

Are you of the opinion that lifetime elision should also be removed? These two features have similar uses and drawbacks. You also can't provide a lifetime explicitly via turbofish when lifetime elision is being used. Sometimes you need to explicitly name them (either to show equivalence or some relationship between multiple lifetimes), but it's pretty rare. Same for impl Trait. Often you don't need to give a type parameter a name. It's probably more common to need to name a type (either to show equivalence or some relationship between multiple type parameters), but it's still somewhat rare.

I think it's important that we have a way to drop down to a lower level, either because you need to use a turbofish (extremely rare for argument parameters) or because you need to name a type. But this feature gives us a great parity between fn foo(x: &Trait) -> &Trait and fn foo(x: impl Trait) -> impl Trait.

@Pzixel
Copy link

Pzixel commented May 21, 2018

Are you of the opinion that lifetime elision should also be removed? These two features have similar uses and drawbacks. You also can't provide a lifetime explicitly via turbofish when lifetime elision is being used. Sometimes you need to explicitly name them (either to show equivalence or some relationship between multiple lifetimes), but it's pretty rare. Same for impl Trait. Often you don't need to give a type parameter a name. It's probably more common to need to name a type (either to show equivalence or some relationship between multiple type parameters), but it's still somewhat rare.

No, lifetime elision is fine, because it's the only way to specify object lifetimes. There is no "easier but more fragile syntax" that allows you to write the very same thing but using a bit less characters.

I love impl Trait, but Rust already have a very nice and powerful generics. Having single way to write "argument of type T" have much higher value than possibility to sometimes inline type declarations. This is why I love impl Trait in return position: it's not possible to express is via existing language features.

In a nutshell: conservative-impl-trait is strictly a language expansion so it's considered to be good, while universal-impl-trait is substitution of existing features thus is considered harmful..

@aturon
Copy link
Member

aturon commented May 21, 2018

We are going to take the unusual step of immediately closing and locking this RFC.

While there are clearly a lot of feelings on all sides of the issue, the fact is that this feature has shipped on the stable channel and is not going to be removed. This is both because of our basic stability guarantees but also because the decision was made in good faith and pursuant to our process, and we stand by that.

After multiple years of RFC and tracking issue discussions (the first one was RFC #105 in 2014!), the Rust Language Design Team ultimately reached a decision to ship this feature. This decision was not reached lightly. We discussed in depth a number of alternatives, including limiting impl Trait to return position as well as using an alternate syntax. Each has its pros and cons, and ultimately a judgement call had to be made. This is the nature of the language design process — indeed, any decision process. Few decisions are clear cut, which is why our process includes a number of points where feedback can be given, including a number of final-comment-period advertisements and the like. As a community, we have made a deliberate choice to slow down development to ensure thorough vetting and input into the process.

To be clear: we understand that there are downsides to this feature, and that some people find those downsides concerning. All of us care deeply about Rust, and it can be distressing to see people in power moving things in a direction you dislike. But, at the end of the day, we have to be able to make — and stick with — decisions, striking a balance between long-running feedback and shipping. Rust 2018 will ship with impl Trait in argument position.

-- @nikomatsakis and @aturon, on behalf of the Rust Language and Core Teams

@aturon aturon closed this May 21, 2018
@rust-lang rust-lang locked as too heated and limited conversation to collaborators May 21, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.