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

Permit impl Trait in type aliases #2515

Open
wants to merge 17 commits into
base: master
from

Conversation

Projects
None yet
@varkor
Copy link
Member

varkor commented Aug 5, 2018

Allow impl Trait to be used in type aliases, resolving the open question in RFC 2071 as to the concrete syntax for existential type. This makes it possible to write type aliases of the form:

type Adder = impl Fn(usize) -> usize;
// equivalent to: `existential type Adder: Fn(usize) -> usize;`

Rendered.

Thanks to @rpjohnst and @Centril for their ideas, discussion and feedback.

@Centril Centril added the T-lang label Aug 5, 2018

In addition, when documenting `impl Trait`, explanations of the feature would avoid type theoretic terminology (specifically "existential types") and prefer type inference language (if any technical description is needed at all).

## Restricting compound `impl Trait` trait aliases
The type alias syntax is more flexible than `existential type`, but for now we restrict the form to that equivalent to `existential type`. That means that, if `impl Trait` appears on the right-hand side of a type alias declaration, it must be the only type. The following compound type aliases, therefore, are initially forbidden:

This comment has been minimized.

@Nemo157

Nemo157 Aug 5, 2018

Contributor

My biggest issue with this restriction is that it makes impl Trait inconsistent between type aliases and everywhere else (excluding the already inconsistent argument position). With existential type there was a simple rule that could be applied to impl Trait in every position except argument position: it introduces a new anonymous existential type in the current context. This rule works perfectly for return position, type declaration in bindings, type aliases, and could work for type declaration of struct/enum members if there wasn't a chance of confusion with argument-position-impl-trait.

This comment has been minimized.

@varkor

varkor Aug 5, 2018

Member

I'm not quite sure I follow your point. This restriction is simply a syntactic one: it is simply intended to sidestep the question of what:

type Foo = (impl Bar, impl Bar);

means for now (because some people expressed unease at this construction in particular).

impl Trait continues to be applicable in exactly the same places as existential type: this rule simply means it can't be used in more complex scenarios than existential type yet.

This comment has been minimized.

@Nemo157

Nemo157 Aug 5, 2018

Contributor

I mean that with existential types it's possible to have a single very simple rule for desugaring impl Trait that covers both:

type Foo = (impl Bar, impl Bar);
let foo: (impl Bar, impl Bar);

but with type Foo = impl Trait; trying to apply the same sort of rule you get to this recursive definition that needs a special case when you use a bare impl Trait in a type alias.

This comment has been minimized.

@varkor

varkor Aug 5, 2018

Member

Ah, I see. Yes, you do have to have a single type alias as a base case if you're using the impl Trait type aliases to desugar. In practice, the desugaring of existential type is effectively replaced by the type alias. That is, the existential type design was originally intended to act as a desugaring for return-position and variable-binding (e.g. let) impl Trait. Using type aliases fills that role: but you can't use it to desugar itself. In practice, I don't think this is important, as there's no practical difference between the existential type itself and its alias.

This comment has been minimized.

@alexreg

alexreg Aug 6, 2018

Why can't it be used to desugar, @varkor?

This comment has been minimized.

@varkor

varkor Aug 6, 2018

Member

Because if you try to desugar each occurrence of impl Trait you would end up trying to desugar:

type Foo = impl Trait;

into:

type Foo = impl Trait;

so you need to have this as a base case.

@Centril
Copy link
Contributor

Centril left a comment

Some nits

Show resolved Hide resolved text/0000-impl-trait-type-aliases.md Outdated
Show resolved Hide resolved text/0000-impl-trait-type-aliases.md Outdated
Show resolved Hide resolved text/0000-impl-trait-type-aliases.md Outdated
Show resolved Hide resolved text/0000-impl-trait-type-aliases.md Outdated
Show resolved Hide resolved text/0000-impl-trait-type-aliases.md Outdated
Show resolved Hide resolved text/0000-impl-trait-type-aliases.md Outdated
@clarcharr

This comment has been minimized.

Copy link
Contributor

clarcharr commented Aug 5, 2018

Just to clarify because I'm not 100% certain on how impl Trait works, these will allow where clauses too, right? In other words, type Thing = impl Trait where Self: Clone.

If I recall correctly, impl Trait doesn't allow this, as it'd be ambiguous if you said fn fun() -> impl Trait where Self: Clone because it's unclear whether the Self: Clone applies to the impl Trait or the fn fun().

So technically where clauses would have to be added to make this equivalent to the existential type syntax. It also solves the problem of where clauses for impl Trait, as you can make a type alias if you need them.

I think the RFC should have some discussion of where clauses. I didn't see any.

@Nemo157

This comment has been minimized.

Copy link
Contributor

Nemo157 commented Aug 5, 2018

@clarcharr what's the intended meaning of where clauses on an impl Trait? I can't work out what type Thing = impl Trait where Self: Clone; would do, or why you would use it.

@clarcharr

This comment has been minimized.

Copy link
Contributor

clarcharr commented Aug 5, 2018

So I just used Self: Clone as an example but a better one might be, for example, impl Iterator where Self::Item: Clone. While in the future this will be doable as impl Iterator<Item: Clone> you need a where clause for this currently.

In general, there are going to be things you can't express without where clauses, and it'd be nice to allow this sort of thing.

@Nemo157

This comment has been minimized.

Copy link
Contributor

Nemo157 commented Aug 5, 2018

Thanks, that clarifies things a lot for me (I think putting a bound directly on Self really threw me since that can be just a + Bound on the trait). And I now remember I actually raised similar points on the RFC that introduced impl Iterator<Item: Clone> (#2289 in case anyone is interested).

In the end I think I decided that that specific case acts identically to impl Iterator<Item = impl Clone>, which I think will cover 90%+ of use cases.

I'm trying to think of useful bounds that can't be written currently, I guess there could be something like impl Iterator where for<'a> &'a Self::Item: SomeTrait maybe?


Relatedly I was playing round with the current implementation and noticed that it allows specifying bounds in the type parameter list but not adding a where clause, that allowed me to come up with an example of a currently compiling -> impl Trait return type that's not representable without where clauses on the type alias (playground)

existential type Bar<T: IntoIterator>: Iterator<Item = <T::Item as IntoIterator>::Item>;

fn bar<T>(a: T) -> Bar<T>
where
    T: IntoIterator,
    T::Item: IntoIterator,
    <T::Item as IntoIterator>::Item: Clone,

so even if Self based where clauses is deemed out of scope since -> impl Trait doesn't require them I hope where clauses for constraining the generic parameters are available.

@clarcharr

This comment has been minimized.

Copy link
Contributor

clarcharr commented Aug 5, 2018

One thing that I also realised is that this could be accomplished by trait aliases:

trait Trait = Bound where for<'a> &'a Self: Bound
type Thing = impl Trait

So maybe where clauses aren't necessary and might in fact overcomplicate stuff. Still worth mentioning in the RFC, though, as this is something the existential type syntax supported that the new syntax won't.

@varkor

This comment has been minimized.

Copy link
Member

varkor commented Aug 5, 2018

Sorry, I'll get the bounds question soon, but in the meantime, I've noticed that this RFC has a major incompatible flaw with the original RFC 2071 and it could potentially mean this syntax suggestion is misinformed.

The current implementation of existential type, which I was using for reference, makes the underlying (inferred) type hidden to the enclosing module. This is consistent with impl Trait and is a motivating factor for the syntax proposed here. However, RFC 2071 explicitly states that the underlying type is transparent within the enclosing module.

As such, existential type and impl Trait are inconsistent with one another. Either:
(a) the feature as implemented right now must be accepted as having the correct semantics
(b) existential type cannot use the impl Trait syntax.

Note that, contrary to some of the original stated motivations, this feature means that existential type cannot be used as a desugaring for impl Trait.

@KodrAus

This comment has been minimized.

Copy link
Contributor

KodrAus commented Aug 5, 2018

However, RFC 2071 explicitly states that the underlying type is visible within the enclosing module.

I did a quick mobile grok of #2071. Is it the reference section you're talking about where the existential type can be used as its resolved type within its declaring module? What's the tradeoff we're making by excluding this transparency from our model here?

@KodrAus

This comment has been minimized.

Copy link
Contributor

KodrAus commented Aug 5, 2018

However, RFC 2071 explicitly states that the underlying type is visible within the enclosing module.

That doesn't seem inconsistent to my mental model actually. I've been thinking of impl Trait in terms of who decides what Trait is?. For return position, the function body decides what Trait is, and that concrete type is visible to the function body. For argument position, the caller decides what Trait is, and that concrete type is visible to the caller. For module position, the module decides what Trait is, and that concrete type is visible to the module.

@varkor

This comment has been minimized.

Copy link
Member

varkor commented Aug 5, 2018

Is it the reference section you're talking about where the existential type can be used as its resolved type within its declaring module? What's the tradeoff we're making by excluding this transparency from our model here?

That's right. If we include the transparency, it's inconsistent with impl Trait, which is confusing as now impl Trait may or may not be transparent depending on the location. If we exclude transparency, then it's a change of behaviour from RFC 2071. This might be solvable, but requires some care.

That doesn't seem inconsistent to my mental model actually. I've been thinking of impl Trait in terms of who decides what Trait is?.

Yes, it's quite subtle. It could be consistent with argument-position, return-position and module-position impl Trait. However, it's not consistent with impl Trait in let bindings (another consequence of RFC 2071 that's not yet implemented). The simplest solution is probably altering impl Trait's behaviour in these circumstances to make it transparent.

@KodrAus

This comment has been minimized.

Copy link
Contributor

KodrAus commented Aug 5, 2018

However, it's not consistent with impl Trait in let bindings

Ah right, I'd totally forgotten about let bindings! In that case, I'd expect the expression part of the binding to decide what Trait is, and that concrete type to be visible only within that expression. For example:

let mut x: impl Debug = {
    if a { 1 }
    else { some_fn_returning_i32() }
};

x = 1; // err: expected `impl Debug` got `i32`
@MajorBreakfast

This comment has been minimized.

Copy link
Contributor

MajorBreakfast commented Aug 24, 2018

I'd like to propose something that might be a bit controversial. We should at least discuss whether we would like to mark the site where the abstract type and the concrete type meet:

type Baz = impl Bar;

fn foo() -> as Baz {
    some_other_fn()
}

I think it would be beneficial because it makes the non-obvious obvious - in particular when the definition of the abstract type isn't right next to the function.

Edit: I just chose the as keyword randomly. The idea here is that we should consider using a marker.

Edit2: It would also make this code (Playground) compile because one could specify which function "sets" the abstract type.

@dhardy

This comment has been minimized.

Copy link
Contributor

dhardy commented Aug 27, 2018

We don't even want to let a module-level existential type represent multiple types.

Can we please stop using the nomenclature existential types? An existential value is a promise that there exists at least one such value. This is not what impl Trait means — it simply means we have an unspecified (or indirectly specified) type meeting certain bounds (the difference being that we do have a single appropriate type in mind; we're just not stating it).

I suggested the syntax unspec type Foo: Trait elsewhere, though type Foo = impl Trait is better (unless we have legitimate reason to use no bounds — but probably not).

Note that I'm taking a very different view to what @MajorBreakfast wrote above — i.e. type Foo = impl Trait should yield a concrete type Foo, not an abstract one — it's just that the definition is found elsewhere (presumably within the same module), just like type inference works today on let v; bindings.


Could we please allow direct usage within type expressions? E.g.

struct S {
    f: impl Fn() -> i32
}
let v: Vec<impl T>;
@Nemo157

This comment has been minimized.

Copy link
Contributor

Nemo157 commented Aug 27, 2018

@dhardy Because of APIT and RPIT impl Trait in fields has two possible interpretations:

struct S {
    f: impl Fn() -> i32
}

Either an unnamed nominal hidden type (what’s the best shorthand if we can’t use existential?)

existential type S_f: Fn() -> i32;
struct S {
    f: S_f
}

Or an unnameable generic type parameter (useless, but consistent with APIT):

struct S<S_f: Fn() -> i32> {
    f: S_f
}
@dhardy

This comment has been minimized.

Copy link
Contributor

dhardy commented Aug 27, 2018

Why was APIT ever allowed in the first place? It doesn't add anything (other than a little sugar) and it's confusing, as seen here. Can we not restrict it to function arguments?

@Nemo157

This comment has been minimized.

Copy link
Contributor

Nemo157 commented Aug 27, 2018

I think with good documentation on why it’s treated like return position instead of argument position allowing impl Trait in fields could be viable, but that would be a separate RFC. Also, let v: Vec<impl Trait> was already RFCed and I believe should work once rust-lang/rust#53542 is merged.

@dhardy

This comment has been minimized.

Copy link
Contributor

dhardy commented Aug 27, 2018

Either an unnamed nominal hidden type (what’s the best shorthand if we can’t use existential?)

How do we talk about variable bindings like let x = foo();? (Automatic) type deduction. As far as I can see, type Foo = impl Trait has only three differences: (1) we didn't use to allow type elision here, and (2) we are specifying some bound, and (3) the type is not transparent (i.e. one cannot assume any more than the bounds).

So perhaps this is just extended type deduction?

How about type elision? Or indirect typing or unspecified types.

@Centril Centril added the I-nominated label Sep 6, 2018

@Centril Centril removed the I-nominated label Sep 20, 2018

@pnkfelix

This comment has been minimized.

Copy link
Member

pnkfelix commented Sep 20, 2018

@varkor Skimming over the RFC text as written here, I don't see any code snippets showing examples of existentials that have generic type/lifetime parameters...

For example, RFC 2071 has the following example of code it alllows:

#[derive(Debug)]
struct MyStruct<T: Debug> { inner: T };

existential type Foo<T>: Debug;

fn get_foo<T: Debug>(x: T) -> Foo<T> { MyStruct { inner: x } }

Could you please expand the RFC text to include an example like this, to make it clear that the type LHS = impl <TRAIT_RHS> form is not limited to just type Ident = impl <TRAIT_RHS> ?


(I do understand that this RFC is just proposing a new syntax for an existing feature. But its still a good idea to try to provide examples of the basic cases that arise of the new syntax. I'd be especially happy if you went so far as to provide code snippets that use type Foo<'lifetime> = impl TraitTwo; as well, in order to provide concrete examples showing that the generics can be lifetime parameters, not just types, even though RFC 2071 neglected to include such examples...)

@pnkfelix

This comment has been minimized.

Copy link
Member

pnkfelix commented Sep 20, 2018

I do note now that the text does include the line:

However, if the function is parameterised, it may be necessary to add explicit parameters to the type alias (due to the return-type being within the scope of the function's generic paramters, unlike the type alias).

So clearly the intent is to allow such things.

But I'd still prefer they get a couple code snippet examples.

@alexreg

This comment has been minimized.

Copy link

alexreg commented Sep 24, 2018

This is looking in a pretty good state now to me. When can we think about FCPing it?

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