-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Conversation
text/0000-impl-trait-type-aliases.md
Outdated
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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some nits
Just to clarify because I'm not 100% certain on how If I recall correctly, So technically I think the RFC should have some discussion of |
@clarcharr what's the intended meaning of where clauses on an |
So I just used In general, there are going to be things you can't express without |
Thanks, that clarifies things a lot for me (I think putting a bound directly on In the end I think I decided that that specific case acts identically to I'm trying to think of useful bounds that can't be written currently, I guess there could be something like 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 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 |
One thing that I also realised is that this could be accomplished by trait aliases:
So maybe |
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 As such, Note that, contrary to some of the original stated motivations, this feature means that |
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? |
That doesn't seem inconsistent to my mental model actually. I've been thinking of |
That's right. If we include the transparency, it's inconsistent with
Yes, it's quite subtle. It could be consistent with argument-position, return-position and module-position |
Ah right, I'd totally forgotten about let mut x: impl Debug = {
if a { 1 }
else { some_fn_returning_i32() }
};
x = 1; // err: expected `impl Debug` got `i32` |
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
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
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
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
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
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
I don't get, when you would use impl trait type aliases after we get trait aliases. Both features would allow very similar things:
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? |
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. |
@HadrienG2 Thanks for clarifying. This way it seems more useful. |
I feel like the "reference-level" explanation is missing some key points that is critical to implement.
|
That was documented in RFC 2071, it's the enclosing module/trait impl. This RFC just changed the syntax used in that existing feature.
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.
Depends what you mean, the second paragraph of 2071's guide section explains it quite well. |
Surely I'm missing something here, but functions already implement 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 type MyIterType = make_iter::Output; Or likewise with async fn foo() -> u32 { todo!() }
type FooFuture = foo::Output; |
@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 Also, 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. |
Allow
impl Trait
to be used in type aliases and associated traits, resolving the open question in RFC 2071 as to the concrete syntax forexistential type
. This makes it possible to write type aliases of the form:Rendered.
Tracking issue.
Thanks to @rpjohnst and @Centril for their ideas, discussion and feedback.