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

Permit impl Trait in type aliases #2515

Merged
merged 22 commits into from
Jul 28, 2019

Conversation

varkor
Copy link
Member

@varkor varkor commented Aug 5, 2018

Allow impl Trait to be used in type aliases and associated traits, 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.
Tracking issue.

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

@Centril Centril added the T-lang Relevant to the language team, which will review and decide on the RFC. 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:
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

@varkor varkor Aug 5, 2018

Choose a reason for hiding this comment

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

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.

Copy link

Choose a reason for hiding this comment

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

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

Copy link
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

@Centril Centril left a comment

Choose a reason for hiding this comment

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

Some nits

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 Show resolved Hide resolved
@clarfonthey
Copy link
Contributor

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
Copy link
Member

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.

@clarfonthey
Copy link
Contributor

clarfonthey 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
Copy link
Member

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.

@clarfonthey
Copy link
Contributor

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
Copy link
Member Author

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
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
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
Copy link
Member Author

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

bors added a commit to rust-lang/rust that referenced this pull request Nov 13, 2019
Push `ast::{ItemKind, ImplItemKind}::OpaqueTy` hack down into lowering

We currently have a hack in the form of `ast::{ItemKind, ImplItemKind}::OpaqueTy` which is constructed literally when you write `type Alias = impl Trait;` but not e.g. `type Alias = Vec<impl Trait>;`. Per rust-lang/rfcs#2515, this needs to change to allow `impl Trait` in nested positions.  This PR achieves this change for the syntactic aspect but not the semantic one, which will require changes in lowering and def collection. In the interim, `TyKind::opaque_top_hack` is introduced to avoid knock-on changes in lowering, collection, and resolve. These hacks can then be removed and fixed one by one until the desired semantics are supported.

r? @varkor
tmandry added a commit to tmandry/rust that referenced this pull request Nov 14, 2019
Push `ast::{ItemKind, ImplItemKind}::OpaqueTy` hack down into lowering

We currently have a hack in the form of `ast::{ItemKind, ImplItemKind}::OpaqueTy` which is constructed literally when you write `type Alias = impl Trait;` but not e.g. `type Alias = Vec<impl Trait>;`. Per rust-lang/rfcs#2515, this needs to change to allow `impl Trait` in nested positions.  This PR achieves this change for the syntactic aspect but not the semantic one, which will require changes in lowering and def collection. In the interim, `TyKind::opaque_top_hack` is introduced to avoid knock-on changes in lowering, collection, and resolve. These hacks can then be removed and fixed one by one until the desired semantics are supported.

r? @varkor
tmandry added a commit to tmandry/rust that referenced this pull request Nov 14, 2019
Push `ast::{ItemKind, ImplItemKind}::OpaqueTy` hack down into lowering

We currently have a hack in the form of `ast::{ItemKind, ImplItemKind}::OpaqueTy` which is constructed literally when you write `type Alias = impl Trait;` but not e.g. `type Alias = Vec<impl Trait>;`. Per rust-lang/rfcs#2515, this needs to change to allow `impl Trait` in nested positions.  This PR achieves this change for the syntactic aspect but not the semantic one, which will require changes in lowering and def collection. In the interim, `TyKind::opaque_top_hack` is introduced to avoid knock-on changes in lowering, collection, and resolve. These hacks can then be removed and fixed one by one until the desired semantics are supported.

r? @varkor
Centril added a commit to Centril/rust that referenced this pull request Nov 15, 2019
Push `ast::{ItemKind, ImplItemKind}::OpaqueTy` hack down into lowering

We currently have a hack in the form of `ast::{ItemKind, ImplItemKind}::OpaqueTy` which is constructed literally when you write `type Alias = impl Trait;` but not e.g. `type Alias = Vec<impl Trait>;`. Per rust-lang/rfcs#2515, this needs to change to allow `impl Trait` in nested positions.  This PR achieves this change for the syntactic aspect but not the semantic one, which will require changes in lowering and def collection. In the interim, `TyKind::opaque_top_hack` is introduced to avoid knock-on changes in lowering, collection, and resolve. These hacks can then be removed and fixed one by one until the desired semantics are supported.

r? @varkor
tmandry added a commit to tmandry/rust that referenced this pull request Nov 15, 2019
Push `ast::{ItemKind, ImplItemKind}::OpaqueTy` hack down into lowering

We currently have a hack in the form of `ast::{ItemKind, ImplItemKind}::OpaqueTy` which is constructed literally when you write `type Alias = impl Trait;` but not e.g. `type Alias = Vec<impl Trait>;`. Per rust-lang/rfcs#2515, this needs to change to allow `impl Trait` in nested positions.  This PR achieves this change for the syntactic aspect but not the semantic one, which will require changes in lowering and def collection. In the interim, `TyKind::opaque_top_hack` is introduced to avoid knock-on changes in lowering, collection, and resolve. These hacks can then be removed and fixed one by one until the desired semantics are supported.

r? @varkor
bors added a commit to rust-lang-ci/rust that referenced this pull request Feb 7, 2022
Lazy type-alias-impl-trait

Previously opaque types were processed by

1. replacing all mentions of them with inference variables
2. memorizing these inference variables in a side-table
3. at the end of typeck, resolve the inference variables in the side table and use the resolved type as the hidden type of the opaque type

This worked okayish for `impl Trait` in return position, but required lots of roundabout type inference hacks and processing.

This PR instead stops this process of replacing opaque types with inference variables, and just keeps the opaque types around.
Whenever an opaque type `O` is compared with another type `T`, we make the comparison succeed and record `T` as the hidden type. If `O` is compared to `U` while there is a recorded hidden type for it, we grab the recorded type (`T`) and compare that against `U`. This makes implementing

* rust-lang/rfcs#2515

much simpler (previous attempts on the inference based scheme were very prone to ICEs and general misbehaviour that was not explainable except by random implementation defined oddities).

r? `@nikomatsakis`

fixes rust-lang#93411
fixes rust-lang#88236
@porky11
Copy link

porky11 commented Oct 2, 2022

I don't get, when you would use impl trait type aliases after we get trait aliases.

Both features would allow very similar things:

  • aliases for complicated function types
  • aliases for multiple traits

So the only thing impl trait type aliases achieve when trait aliases exist is allowing to not write "impl" in functions, which use impl trait obfuscating a type being opaque, right?

@HadrienG2
Copy link

From my understanding, this feature is about asserting that multiple functions return the same "impl Trait" type.

Normally, every instance of impl Trait is treated as a different type, which means you cannot e.g. take a variable that used the output of an impl Trait returning function and overwrite its value with the result of a different function returning an impl of the same trait.

@porky11
Copy link

porky11 commented Oct 3, 2022

@HadrienG2 Thanks for clarifying. This way it seems more useful.

@earthengine
Copy link

earthengine commented Oct 3, 2022

I feel like the "reference-level" explanation is missing some key points that is critical to implement.

  • When should the type alias being instanced/consolidated? The answer can be: same file, same mod, same crate, or global. Each options would have its advantages/drawbacks.
  • Should the type alias being declared with pub? Natually we would say why not, but it could have unwanted effects.
  • Can an external crate declare functions returns the same type? Again something unexpected can happen if we allow this.

@Nemo157
Copy link
Member

Nemo157 commented Oct 3, 2022

When should the type alias being instanced/consolidated? The answer can be: same file, same mod, same crate, or global. Each options would have its advantages/drawbacks.

That was documented in RFC 2071, it's the enclosing module/trait impl. This RFC just changed the syntax used in that existing feature.

Should the type alias being declared with pub? Natually we would say why not, but it could have unwanted effects.

Orthogonal to this and 2071, you could use private TAIT to give names for private struct fields, so there's no reason to restrict what visibility can be applied.

Can an external crate declare functions returns the same type? Again something unexpected can happen if we allow this.

Depends what you mean, the second paragraph of 2071's guide section explains it quite well.

@josephg
Copy link

josephg commented Sep 30, 2024

Surely I'm missing something here, but functions already implement FnOnce which provides an f::Output associated type naming the function's return value:

pub trait FnOnce<Args> where Args: Tuple {
    type Output;
    // ...

But I can't use it. This doesn't work:

fn make_iter() -> impl Iterator<Item=()> { todo!() }

struct Foo {
    iter: make_iter::Output,
}

Wouldn't it be much simpler to just enable the use of FnOnce::Output? This would obviously enable all the use cases of this RFC, since you could use a type alias it if you wanted to:

type MyIterType = make_iter::Output;

Or likewise with async fn:

async fn foo() -> u32 { todo!() }

type FooFuture = foo::Output;

@compiler-errors
Copy link
Member

compiler-errors commented Sep 30, 2024

@josephg: This is the wrong place to ask this question, I think 😅 since this RFC has long been closed

The answer to your question is that this RFC describes named existential types in general, which really don't have too much to do with naming fn_item::Output since they can appear and be constrained in many more positions than simply a function's bare return type. I can write type Foo = impl Sized; fn bar() -> Box<Foo> { ... } which isn't afaict expressible without some additional syntax to project bar::Output to the type argument of Box.

Also, ::Output is not as flexible as it might initially seem, since the output for a function is worst-case a higher-ranked type (e.g. the type foo::Output when foo is fn foo(x: &i32) -> &i32). There are also complications with name resolution since foo exists in the value namespace, and associated type lookup happens in the type namespace, among probably other complications I am forgetting. This is all somewhat related to why #3654 ended up taking the shape it did.

Given that this RFC thread is very old, discussion should not continue here since it likely pings many people who have probably forgotten about commenting here, and so I'm going to lock it. I think the best place to ask more questions like this would be either on https://internals.rust-lang.org or on Zulip.

@rust-lang rust-lang locked as resolved and limited conversation to collaborators Sep 30, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A-impl-trait impl Trait related proposals & ideas A-syntax Syntax related proposals & ideas A-traits Trait system related proposals & ideas A-type-alias Type alias related proposals & ideas A-typesystem Type system related proposals & ideas disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this RFC. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.