Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upPermit impl Trait in type aliases #2515
Conversation
varkor
added some commits
Aug 3, 2018
Centril
added
the
T-lang
label
Aug 5, 2018
varkor
referenced this pull request
Aug 5, 2018
Open
Tracking issue for `impl Trait` (RFC 1522, RFC 1951, RFC 2071) #34511
Nemo157
reviewed
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.
This comment has been minimized.
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.
This comment has been minimized.
varkor
Aug 5, 2018
Author
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.
This comment has been minimized.
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.
This comment has been minimized.
varkor
Aug 5, 2018
•
Author
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.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
varkor
Aug 6, 2018
Author
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.
text/0000-impl-trait-type-aliases.md Outdated
text/0000-impl-trait-type-aliases.md Outdated
text/0000-impl-trait-type-aliases.md Outdated
text/0000-impl-trait-type-aliases.md Outdated
text/0000-impl-trait-type-aliases.md Outdated
text/0000-impl-trait-type-aliases.md Outdated
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
@clarcharr what's the intended meaning of where clauses on an |
This comment has been minimized.
This comment has been minimized.
|
So I just used In general, there are going to be things you can't express without |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
One thing that I also realised is that this could be accomplished by trait aliases:
So maybe |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
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? |
This comment has been minimized.
This comment has been minimized.
That doesn't seem inconsistent to my mental model actually. I've been thinking of |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
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` |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Feb 26, 2019
|
@scottmcm I would expect the following desugarings: type Foo = Arc<impl Trait>;--> type Foo<T: Trait> = Arc<T>;(analogous to APIT in fns) trait Bar = Add<impl Trait>;--> trait Bar<T: Trait> = Add<T>;(again, analogous to APIT in fns) trait Qux = Iterator<Item = impl Trait>;--> trait Qux = Iterator<Item = _0>;
existential type _0: Trait;(analogous to RPIT in fns) The third case also has a relationship to associated type bounds. For example, |
This comment has been minimized.
This comment has been minimized.
|
@alexreg your interpretation implies that one should be able to write: type Foo = Arc<impl Trait>;
type FooX = Foo<X>; // where X: TraitSo, suddenly the definition of For what it's worth, I think the adoption of APIT was misguided; however I recognise that is hard to change now. |
This comment has been minimized.
This comment has been minimized.
|
@rfcbot concern thinking My thinking on this has shifted to some degree recently but I haven't had time to think about it in a sustained and serious manner. I don't have the time to think about it just now, registering a concern until I do. |
This comment has been minimized.
This comment has been minimized.
|
@dhardy if it is fully consistent with APIT then type Foo = Arc<impl Trait>;
type FooX = Foo<X>;should give an error like
and instead |
This comment has been minimized.
This comment has been minimized.
|
Centril made a very nice argument about why this may be the best candidate syntax for existential types, and now we have confusion over whether the syntax is used for existentials (as in RPIT) or sugar for a hidden type parameter (as in APIT). Thus we have the following options:
|
This comment has been minimized.
This comment has been minimized.
|
One reason that the RPIT/APIT distinction for type aliases doesn't make sense is that the To extend one of @scottmcm's examples further, consider: type Foo = Arc<impl Iterator<Item = impl Debug>>;in this case if the type inside type FooIter = impl Iterator<Item = impl Debug>;
type Foo = Arc<FooIter>;now both of the Overall it seems like a type/trait alias is an output type of the module and therefore should always be treated the same as an RPIT type, meaning |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Feb 26, 2019
Not really. In
This has been expressed by a bunch of people, but it's far too late... there are pros and cons either way. |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Feb 26, 2019
|
@Nemo157 Your desugaring is wrong, I believe. type Foo = Arc<impl Iterator<Item = impl Debug>>;--> type Foo<T: Iterator<Item = _0>> = Arc<T>;
existential type _0: Debug;That's certainly what I would expect. |
This comment has been minimized.
This comment has been minimized.
|
@alexreg that’s not a desugaring, that’s a manual splitting of the type alias, maybe because the author has decided that being able to name the inner type individually would be useful. This would work under the “always existential” rule, but wouldn’t with the type of the generic varying like you mention. |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Feb 26, 2019
|
@Nemo157 Oh, I see what you mean. Yes... we need to think about how best to teach that, since that intuitive splitting does of course change semantics. |
This comment has been minimized.
This comment has been minimized.
rpjohnst
commented
Feb 26, 2019
This is a misunderstanding of the term "existential" and the whole reason we are moving away from that description. APIT and hidden type parameters are existential- the types The proposed interpretation is instead " |
This comment has been minimized.
This comment has been minimized.
|
@rpjohnst I agree that "existential" is not really the correct terminology (if you look up 20 or so posts, you'll see I suggested some alternatives). It does appear to be what everyone is using, but you are right: calling these "opaque types" is more accurate. There is however a very important difference. E.g. is the following legal? Is type T = impl Trait;
struct S {
value: T
}
// ... some more code to constrain the type of T sufficientlyWhat I would personally like to see is |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Feb 26, 2019
|
@dhardy I would expect that in your example |
This comment has been minimized.
This comment has been minimized.
|
I just realised the issue I have with @alexreg's desugaring above that was hiding the use of APIT. Specifically, concrete types should be transparent to the APIT/RPIT distinction (as evidenced by my playground above): type Foo = Arc<impl Trait>;should be the equivalent of what is written today as type Foo = Arc<_0>;
existential type _0: Trait;This follows from treating The interesting question is what should happen when you have nested type Bar = impl std::ops::Add<impl Into<i32>, Output = impl std::fmt::Display>;
fn bar() -> Bar { ... }this could take the form that @alexreg expands above where input type parameters to traits get flipped, but that seems to require introducing a non-local hidden generic type parameter onto functions (it would even be possible to introduce this hidden parameter onto a function outside the current module if it returns a value it got from calling a function inside the module) type Bar<_0: Into<i32>> = _1<_0>;
existential type _1<_2: Into<i32>>: std::ops::Add<_2, Output = _3>;
existential type _3: std::fmt::Display;
fn bar<_4: Into<i32>>() -> Bar<_4> { ... }EDIT: Unless higher ranked type-generic bounds were a thing, maybe?type Bar = _1;
existential type _1: for<_2: Into<i32>> std::ops::Add<_2, Output = _3>;
existential type _3: std::fmt::Display;
fn bar() -> Bar { ... }Looking at what happens if you try and write the equivalent today, as an RPIT: fn bar() -> impl std::ops::Add<impl Into<i32>, Output = impl std::fmt::Display> { ... }gives existential type Bar: std::ops::Add<impl Into<i32>, Output = impl std::fmt::Display>;gives the same error on both uses of |
This comment has been minimized.
This comment has been minimized.
rpjohnst
commented
Feb 26, 2019
This matches the proposal. That use of |
This comment has been minimized.
This comment has been minimized.
If we map out where the RFC would naturally lead, it would desugar to
Under this RFC, existential type _0: Debug;
existential type _1: Iterator<Item = _0>;
type Foo = Arc<_1>;@varkor, can you please clarify this in the reference (and in general how nested impl traits work where they are allowed (but notably not
From the perspective of type inference with opacity, let's desugar your example (with defining use type T = forget(?0, Trait);
struct S { value: T }
pub fn foo() -> T { 42u8 }Here, Type aliases are aliases and therefore every monomorphic use of them should refer to the same type. Combined with
Probably not :) If we have
This is an error for the same reason that |
golddranks
referenced this pull request
Feb 28, 2019
Closed
Don't use impl Connect in return types #17
This comment has been minimized.
This comment has been minimized.
One great property of this RFC as written is that it is truly just resolving a syntactic question, without attempting to add any new expressiveness on top of the current implementation. Correct me if I misunderstand what @Centril wrote, but it seems like the call for a final comment period is suggesting an implicit amendment to the RFC that breaks that property.
I understand the goal of trying to strive to maximize uniformity and expressive power. But I worry about an overall issue in the RFC process, where an implicit change is proposed to the RFC via a single sentence in 2,000 word summary comment (a comment that is otherwise just condensing the dialogue up to this point, and not attempting to introduce new ideas).
Having said that, I don't care enough about the potential for process-breakdown to actually block the FCP on this procedural matter. I just wanted to voice my opinion on the matter. |
This comment has been minimized.
This comment has been minimized.
|
@rfcbot reviewed |
This comment has been minimized.
This comment has been minimized.
To add to this point, I think there is no disadvantage to leaving the point in question as an open question. One straightforward possibility is to add an additional feature gate that permits this extended use of |
This comment has been minimized.
This comment has been minimized.
It suggests resolving the existing unresolved question. To make this clearer and to ensure that the document is revised, I'll add concerns to that end: @rfcbot concern resolve-unresolved-question-in-favor-of-allowing-it
I think this remains the case. E.g. consider: #![feature(existential_type)]
existential type _0: core::fmt::Debug;
existential type _1: core::fmt::Debug;
type _2 = (_0, _1);
fn foo() -> _2 { (1, 2) }
existential type _3: core::fmt::Debug;
existential type _4: Iterator<Item = _3>;
fn bar() -> _4 { 0..1 }
fn main() {}This compiles fine on nightly and would be rewritten as: type _2 = (impl core::fmt::Debug, impl core::fmt::Debug);
fn foo() -> _2 { (1, 2) }
type _4 = impl Iterator<Item = impl core::fmt::Debug>;
fn bar() -> _4 { 0..1 } |
This comment has been minimized.
This comment has been minimized.
vi
commented
Mar 26, 2019
•
|
Should there be a rule that Consider two APIs: pub type Thing = impl Traitor;
pub type ThingR = Result<Thing, ()>;pub type ThingR = Result<impl Traitor, ()>;First one is OK. But in the second one it is less obvious how to make Is using non-straightforward definition of existential public type a case of private-in-public? |
This comment has been minimized.
This comment has been minimized.
Just as you may write: fn foo() -> Option<impl Iterator<Item = impl Debug>> { Some(0..1) }so too should you be able to write: type FoosType = Option<impl Iterator<Item = impl Debug>>;
fn foo() -> FoosType { Some(0..1) }I think there's little justification to intentionally make the language less composable in this way. It doesn't make the language simpler. Rather, a surprising ad-hoc rule is imposed that people will need to learn.
It should not be any less obvious than
No more than |
Centril
added
the
I-nominated
label
Mar 27, 2019
This comment has been minimized.
This comment has been minimized.
|
The RFC should explicitly state (in the summary and in the motivation) that this covers |
varkor
added some commits
Mar 28, 2019
This comment has been minimized.
This comment has been minimized.
|
I've clarified the semantics for multiple Edit: after discussion with @Centril, I've resolved the final question in the RFC, permitting multiple occurrences of |
varkor commentedAug 5, 2018
•
edited
Allow
impl Traitto 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.
Thanks to @rpjohnst and @Centril for their ideas, discussion and feedback.