[lang-team-minutes] stabilization and future of impl Trait

You have to be a little indirect: Rust Playground

So the solution right now is that you can only use the type abstractly so that your method will remain valid with any specialized impl? That makes sense, but its hard to construct a Future that way, so I think it presents the same problems.

I just wanted to make the point that associated type specialization is usable.

@aturon Why does impl Trait in argument position need to be clear before stabilizing impl Trait in return position?

It seems to me that adding a short-hand notation for generics is completely orthogonal to supporting existential types in return type position.

Making impl Trait sometimes mean “generics” and sometimes mean “existential” could also be controversial. If one thing can be learned from the dyn Trait RFC is that reusing syntax to mean different things might lead to issues down the road.

EDIT: for me the whole feature would be complete when I can use existentials for local variables, associated types, and structs. I think that:

let i : impl Iterator = foo(a, b).bar(c).moo(f); 
let v : Vec<impl Bar> = i.collect();

makes code more readable, but that seems like something that could be added as a follow up to the minimal impl Trait RFC.

1 Like

The main question is whether we would want to adopt a distinct syntax (e.g., any Trait vs some Trait, as has been proposed). If so, we may regret having stabilized impl Trait.

That said, I currently feel that we should stick to a "DWIM" sort of system, particularly since we can use some notion of variance to help guide us in our defaults (but we would presumably, eventually, want some sort of explicit form as well). I know @aturon has some thoughts along these lines, though I don't recall at this moment what they were. =)

As an example, the impls in this signature probably want to play distinct roles:

fn foo(x: impl Fn(impl Bar))

Using two keywords, it might be fn foo(x: any Fn(some Bar)), meaning in particular that the closure body doesn't get to pick which type of Bar it accepts. I think @aturon simply planned to use variance (in types) and the () sugar (in traits) to drive this system?

The distinction becomes more accute with this example:

// You want: T: Into<U>, U: Bar
foo(x: impl Into<impl Bar>)
// You want: T: for<U: Bar> From<U>
foo(x: impl From<impl Bar>)

@aturon sort of convinced me with “the second one just won’t have a shorthand syntax” during Rust Belt Rust, but I remain conflicted about the whole enterprise.

4 Likes

I think it would only be a problem if we want to use the same syntax for existentials and as short-hand for generics.

I am with @withoutboats in that reusing the same syntax would be controversial. I think impl Trait is actually very good, because it is in type position and means "here is a single type that implement this Trait". In argument position, it would mean "generics" with is more like "for any combinations of types that implement these traits". Using any sounds nice, using nothing would be better but we cannot do that. Anyhow, if we settle on that we want a different syntax for that no matter what, then impl Trait can proceed.

In particular, I remain to be convinced of the benefits of a short-hand syntax, since all the arguments I've heard are very weak.

C++ added a short-hand syntax recently (e,g, OutputIterator copy(InputIterator begin, InputIterator end, OutputIterator out);) and it pays of huge because:

  1. C++ generic syntax is very verbose, so there is a lot to win by using any shorter syntax
  2. They can introduce this syntax without adding keywords (for non-trait objects, for trait-objects they use virtual Concept), so the syntax is really short.
  3. They can make the syntax always do the right thing because C++ "type system" is simpler. They reuse it for generics in argument position, and for "loose existentials" in return type, variable type, and when passing a type to a generic, positions (Concept bar(); std::vector<Concept> foo = { bar(); };).

In Rust, however, the generic syntax is already pretty short (considering all that it can do). Adding a shorter syntax for generics requires a contextual keywords, so it can never be as short as C++. Finally, the syntax cannot be made to always do the right thing, so the longer syntax will probably be need to be learned and known to disambiguate. That is, there is not so much to win, we cannot push it as far as C++, and beginners will probably need to learn both syntaxes anyways.

I think the teachability argument is also particularly weak. Those coming from languages with generics (like Java, C++, Haskell...) won't have many problems with the "long" syntax we currently have. Those coming from dynamic languages without generics don't even know what kind of problems generic do solve since they don't have them in their languages. This is hard on them no matter what we do, I worry about the shorthand syntax making this actually harder, since now you have to teach them two different syntaxes, and the situations in which they can use one and they must use the other, and they are going to ask why.

I would love a short-hand syntax that is really short and always does the right thing to exist in Rust. It just seems to me that finding that, if it exists, is going to take way more time than finishing all the other use cases of impl Trait, which are already complicated enough.

In particular, impl Trait allows doing things that cannot be currently done on Rust. The short-hand would just be sugar, it doesn't add anything new. Usability is very important, but I still have a hard time justifying lack of progress on impl Trait for a short-hand syntax that might not even exist.

6 Likes

I still prefer any Trait and some Trait and I’d argue that the distinction is only too subtle if it’s never presented in the first place.

In fact, I’d go further and say that any/some could be a powerful learning tool, allowing people to write certain kinds of generic code and reason about them without having to understand explicitly named universals/existentials (the latter of which we don’t have yet) or, worse, variance (or whatever contextual rules impl Trait might have).

cc @steveklabnik @jntrnr

16 Likes

FWIW, as someone whose been programming in managed languages for my entire professional career, the conversation around any/some did in fact teach me the difference between existentials and universals. It also probably took me a month or two to fully grok.

In my mind the sentence "any given input parameter will result in some exact output parameter" now draws an important distinction. I'm not sure how to weigh that against the learning phase for (probably?) lots of folks that will result in the cargo-culting of "use any in parameter position and some in return position".

2 Likes

Part of the problem is the Option::Some ambiguity, right? Has any/one been proposed yet?

one by itself isn’t as existential as some, but if it’s taught at the same time as any it seems like it might be just as clear. Plus it’s one character shorter than impl, which is a huge win.

1 Like

Another alternative we've considered which I think is mentioned upthread is my instead of some. This conflicts a little bit with the language we use to talk about ownership.

Both @eddyb and @gnzlbg raise valid points. In particular, any/some clearly complement each other and also i agree we won’t be able to do the simpler thing as c++ does because rust is more expressive.

impl trait syntax is neither here nor there as it lacks the symmetry. We should either give up on the short syntax and have long syntax for both or adopt the any/some modifiers. Having only short syntax for existentials and only long syntax for universals is plain inconsistent.

An additional data point for comparison is that both c# and java have similar dichotomies. The former has in / out modifiers and the latter has wildcards which also provide two opposites: <? extends Foo> / <? super Foo>

Either way, the analogy is close enough to help teach for their users.

2 Likes

I figure that since impl Trait is just a shorthand for ∃⟨T: Trait⟩ T, its meaning should follow as a direct consequence of that:

X → ∃⟨T: Trait⟩ T           ≃   ∃⟨T: Trait⟩ (X → T)

(∃⟨T: Trait⟩ T) → Y         ≃   ∀⟨T: Trait⟩ (T → Y)

((∃⟨T: Trait⟩ T) → X) → Y   ≃   (∀⟨T: Trait⟩ (T → X)) → Y
                            ≃   ∃⟨T: Trait⟩ ((T → X) → Y)

Maybe in the future impl could be extended for more expressive existentials? E.g. impl<X: Trait> (X, fn(X) -> X).

I think a shorthand syntax improves readability not only by making it shorter, which by itself isn’t that important, but most importantly by making the order more intuitive by keeping it left to right.

When reading fn open(path: any AsRef<Path>) -> File, I think "the function open takes an argument path with can be any AsRef<Path> and returns a File".

When reading fn open<T: AsRef<Path>>(path: T) -> File, I think “the function open is generic over a type T which should implement AsRef<Path>, and takes an argument path of type T and I’ve now forgotten what T was and what bounds it has so I need to go back left to figure it out”. This is even worse when using where clauses, since I need to look left and right.

For a simple function like open it may seem trivial, but a generic argument in 3rd or 4th position, or a generic return type becomes very distant from the type variable’s definition. Of course the full syntax is still needed in some cases, but we should make the most common case as readable as possible.

One issue that I haven’t seen mentioned is whether type inference can still be overidden when using the shorthand syntax. Can I use open<&str>(foo) even if open is defined as fn open(path: any AsRef<Path>) -> File ?

Finally I think using the same keyword for universals and existentials is really confusing and limiting, given that universals are allowed in return types as well. I find any/some really neat, but the confusion with Option::Some is unfortunate. any/one would work too I guess.

While short to type, impl is annoying to read/pronounce. I can either read it as “implements” which is quite long, or “impol” which feels very uncomfortable given it isn’t an english word.

11 Likes

This might be a bit late, but I am wondering whether there are any plans to allow explicitly naming impl Trait types. There are a lot of situations in Rust where you are still required to explicitly specify types, and it is bad enough already that you can’t name closures. I fear that things will get a lot worse once impl Trait is stabilized, because libraries will start using it everywhere.

For example, today if I want to refer to the chars iterator returned by String, I can just write Chars<'a>. If it were using impl Trait, that would be impossible. Allowing people to omit type definitions in more places is not a complete substitute for adding the ability to define types in the first place.

4 Likes

Regarding the syntax bikeshed, I prefer “some Trait” over “impl Trait” regardless of whether we ever end up with an “any” keyword for universals. It’s not “some/any vs impl” but just “some vs impl”, imo. I do not think stabilization of impl Trait should be blocked on deciding what if any sugar we should have for existing generics, especially since impl Trait is an actual new feature, not just sugar. The only thing it should be blocked on is which keyword to use for impl Trait itself.

So far, all of the proposals I’ve seen for sugar on regular generics either mistakenly assume impl Trait is just sugar and simply propose using it in more places, or can only be applied to a small subset of generics so that the “now you have to learn two syntaxes” argument applies. I strongly agree with @gnzlbg that a genuinely worthwhile generics sugar probably doesn’t exist, or will take way too long to find if it does exist and isn’t worth waiting for.

I’d be happy with almost any keyword being chosen, but I prefer “some” over “impl” simply because I’ve seen way too many people assume “impl” is pure sugar and not an actual feature. For whatever reason, it’s clearly not getting the point across. Even without an “any” keyword, I believe “some” would do a much better job of implying that something much more interesting is going on than “a thing that implements a trait goes here”. Not everyone will be able to guess that it means existential types, but at least they’d be more likely to guess it has some kind of unique semantics. (the other suggestions I’ve seen like “my” all seem significantly less likely to successfully imply existential types than “some”, but I wouldn’t rule any of them out)


@Storyyeller iirc, one of the biggest motivations for impl Trait is being able to return things like closures or iterators without having to name their types (or box them). Wouldn’t that improve the issues you’re talking about?

3 Likes

In some ways it would improve the situation, in others, it would make it worse. As long as there is ANY situation in which it is necessary to explicitly name a type, it is desirable to be able to name types. Causing a proliferation of anonymous types without fixing that is likely to be problematic. IMO, there should be a way to name closure types, but not making the problem worse would be a good first step.

Maybe the associated type desugaring could be used as a way to explicitly name impl Trait types.

I expressed a similar concern before, that favoring impl Trait in a library will make it harder for users in some cases. Return anonymity is nice for function authors, but harder for type authors that want to store it, unless we can name that output.

Hmm, actually, does something like <foo<'a, T> as FnOnce>::Output work?

Assuming you mean a function, not today, because foo is a value with an unnameable unique type, not a type itself.

SImilarly I also opened this issue (which I already linked at the beginning of the post (long forum posts tend to behave as infinite loops)), but it didn't go anywhere because I'm not a language designed and don't know what to push forward.

Even if it worked it wouldn't be a good solution.

Image this:

fn build_foo() -> Foo {
    Foo {
        iter: (0 .. 3).map(|n| n + 1),
        ... other fields ...
    }
}

How do you express the type of iter that you want to put in the Foo struct? Do you really want to create a dedicated function that just contains (0 .. 3).map(|n| n + 1) in order to be able to express its type?