Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upNamed existentials and impl Trait variable declarations #2071
Conversation
aturon
added
the
T-lang
label
Jul 20, 2017
aturon
self-assigned this
Jul 20, 2017
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
aturon
Jul 20, 2017
Member
Thanks so much, @cramertj, for taking this on!
I wanted to follow up on two points that @nikomatsakis, @eddyb, @cramertj and I have been discussing but didn't make it in full detail in the RFC.
Going fully expressive
First, the question of pursuing this feature as a next step, rather than adding direction support for fn-level impl Trait in traits: as we explored the design space, it turns out that the design for this "more advanced" feature is actually far more straightforward than for the "simpler", more sugary version. In addition, there are some open questions around the sugary version that we could get insight into with more experience.
I do want to emphasize that I personally am uncomfortable with the situation where you can use impl Trait in fn signatures, unless you are defining or implementing a trait. I think we should strive to support that ASAP. But @cramertj has convinced me that it's wisest to start with this RFC as the first step.
Implementation concerns
The RFC strives to stay pretty high-level in terms of the specification, but it has some pretty significant implementation impact. In particular, the fact that a single type Foo = impl SomeTrait; definition may be determined jointly by multiple functions in a module implies that we're doing some amount of module-level type inference or checking. However, this is far less scary than it might sound at first.
The idea we've discussed involves doing type inference as usual for each function, while treating instances of an impl Trait type alias as an inference variable. In today's system, for type checking to succeed for a function, by the end of the process we must be able to fulfill all outstanding "obligations" (basically: things to verify about the types, most commonly checking for trait implementations). In this setup, though, we may not have enough information within a single function to know for sure that all obligations have been fulfilled, since we may not know the full identity of the type alias.
There are a number of options for how to proceed, falling on a spectrum. Here are the two extremes:
-
At the most conservative, we could force the type variable we introduced to be fully resolved by the end of type checking. That would mean each function using the alias, by itself, must contain enough information to fully infer the concrete type for the alias. In this approach, we'd still be able to require that all obligations are fulfilled by the end of type checking a function. As a final step, we then ensure that all functions using the alias agree on the concrete type they nailed down.
-
At the most liberal, we could fulfill as many obligations as possible when checking each function, and then store the remaining ones. Then, after checking all of the functions within a module, we would combine their remaining obligations and ensure that they can all be resolved. That is, in effect, module-level type inference, but done in a way that pushes as much locally as possible. The impact on incremental compilation is not entirely clear.
The RFC is deliberately leaving the precise resolution of these questions up in the air, since they are best resolved through implementation and experimentation.
I personally think we should start with the most conservative approach and go from there.
|
Thanks so much, @cramertj, for taking this on! I wanted to follow up on two points that @nikomatsakis, @eddyb, @cramertj and I have been discussing but didn't make it in full detail in the RFC. Going fully expressiveFirst, the question of pursuing this feature as a next step, rather than adding direction support for I do want to emphasize that I personally am uncomfortable with the situation where you can use Implementation concernsThe RFC strives to stay pretty high-level in terms of the specification, but it has some pretty significant implementation impact. In particular, the fact that a single The idea we've discussed involves doing type inference as usual for each function, while treating instances of an There are a number of options for how to proceed, falling on a spectrum. Here are the two extremes:
The RFC is deliberately leaving the precise resolution of these questions up in the air, since they are best resolved through implementation and experimentation. I personally think we should start with the most conservative approach and go from there. |
text/0000-impl-trait-type-alias.md
| // Type `Foo` refers to a type that implements the `Debug` trait. | ||
| // The concrete type to which `Foo` refers is inferred from this module, | ||
| // and this concrete type is hidden from outer modules (but not submodules). | ||
| pub type Foo: impl Debug; |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
cramertj
Jul 21, 2017
Member
Yes-- same for the other one. Thanks for catching that. I'll fix it as soon as I get to a computer.
cramertj
Jul 21, 2017
Member
Yes-- same for the other one. Thanks for catching that. I'll fix it as soon as I get to a computer.
text/0000-impl-trait-type-alias.md
| inner: T | ||
| }; | ||
| type Foo<T> -> impl Debug; |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
scottmcm
Jul 21, 2017
Member
impl Trait in let, const, and static looks amazing
Can I cast (or type ascribe) impl Trait?
let displayable = "Hello, world!" as impl Display;Can an impl trait alias be self-referential?
type Foo = impl Add<Foo, Output=Foo>;The impl Trait type "alias" syntax somewhat scares me. While technically they do create synonyms for some type, they're very different in that manually substituting the right hand side in place of the type alias into signatures changes behaviour. I'm also not certain what type Foo<T> = impl Bar<T>; would (eventually) mean in the "module-level type inference" model. Would it infer a single concrete type constructor?
|
Can I cast (or type ascribe) let displayable = "Hello, world!" as impl Display;Can an impl trait alias be self-referential? type Foo = impl Add<Foo, Output=Foo>;The |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
eddyb
Jul 21, 2017
Member
Can an impl trait alias be self-referential?
In the current implementation, at least, yes, as the trait bounds are associated to, but distinct from, the type itself, i.e. it's something like this in the compiler:
type_of(Foo) = Anon0;
predicates_of(Anon0) = [Anon0: Add, <Anon0 as Add>::Output == Anon0];
In the current implementation, at least, yes, as the trait bounds are associated to, but distinct from, the type itself, i.e. it's something like this in the compiler: type_of(Foo) = Anon0;
predicates_of(Anon0) = [Anon0: Add, <Anon0 as Add>::Output == Anon0]; |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
cramertj
Jul 21, 2017
Member
Can I cast (or type ascribe) impl Trait?
This RFC wouldn't allow either of those. It's not totally obvious to me what either let x = "Hello world!" as impl Display; or let x = "Hello world!": impl Display; would mean. My gut reaction is that the type ascription (:) one should behave the same as an impl Trait let binding, which is basically a no-op except for providing some hints to type inference.
This RFC wouldn't allow either of those. It's not totally obvious to me what either |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
cramertj
Jul 21, 2017
Member
I'm also not certain what type Foo<T> = impl Bar<T>; would (eventually) mean in the "module-level type inference" model. Would it infer a single concrete type constructor?
Yes, Foo<T> must resolve to a single concrete type constructor:
trait MyTrait {}
type Foo<T> = impl MyTrait;
struct MyStruct<A, B, C> {
a: A,
b: B,
c: C,
}
impl<A, B, C> MyTrait for MyStruct<A, B, C> {}
fn foo<T>(t: T) -> Foo<T> {
// This tells the compiler that `for<T> Foo<T> == MyStruct<i32, T, &'static str>`
MyStruct { a: 1i32, b: t, c: "" }
}
Yes, trait MyTrait {}
type Foo<T> = impl MyTrait;
struct MyStruct<A, B, C> {
a: A,
b: B,
c: C,
}
impl<A, B, C> MyTrait for MyStruct<A, B, C> {}
fn foo<T>(t: T) -> Foo<T> {
// This tells the compiler that `for<T> Foo<T> == MyStruct<i32, T, &'static str>`
MyStruct { a: 1i32, b: t, c: "" }
} |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Ixrec
Jul 22, 2017
Contributor
My way-more-than-two cents on this RFC:
I am strongly in favor of the functionality being proposed here.
This makes it a lot easier to properly distinguish public APIs from implementation details in a way the compiler reliably enforces, makes it far more feasible to work with otherwise unnameable types like closures or messy "I really don't care what's in here" types like iterator combinators, and the syntax seems about as concise, obvious and ergonomic to me as it could possibly get.
This seems very relevant to the "publicly unnameable types" issue, but the RFC never mentions that.
I have no idea how widely known that issue is, or what everyone else is calling it these days, so I should probably explain what I mean by it:
First, backstory: The only objection I'm aware of to having impl Trait be in the language at all is that it makes unnameable types more common. In particular, it causes types which were nameable within a function or module to become unnameable outside that function or module. I'm calling those "publicly unnameable types" to distinguish them from "everywhere unnameable types" like closures (which this RFC does mention). Note that being able to name a type does not mean being able to rely on that type never changing. For instance, if your library has a function returning i32, and my code puts that i32 in one of my structs, today I need to be able to write "i32" as part of my struct's type definition. If you change that i32 to impl Debug, then I can no longer rely on you always returning i32 anymore (which IS a good thing), but I also can't put your i32 in one of my structs anymore because Rust doesn't provide a way to say "whatever type that function returns" (which is NOT a good thing).
Previously, I thought the only solution to this would be adding a typeof operator. Then I could write typeof(foo(x, y, z)) in my struct definition to tell the compiler I want whatever foo's concrete return type is. But in this RFC, every new usage of impl Trait being proposed comes with a name, which seems like a far better solution to that problem since it doesn't require the massive syntax bikeshed that typeof would (e.g., should I have even put "x, y, z" in that pseudocode just now?) and we automatically get all the trait bounds needed for type safety without any duplicate where clauses in my struct definition (I'm not actually sure if typeof would require extra annotations like that, but I assume it wouldn't be quite as trivial/ergonomic as Trait::FooType).
However, this RFC also alludes to a "sugar" for "impl Trait in traits", though it's never stated exactly what that is since it's not part of this proposal. I assume this hypothetical feature would mean making code like this:
trait Foo {
fn foo() -> impl Debug;
}
be sugar for this:
trait Foo {
type __FooSecretAssocType1__ = impl Debug;
fn foo() -> __FooSecretAssocType1__;
}
If this sugar is added, then using it makes the type unnameable again. So there's an argument we should never actually add this sugar unless we also add something like typeof, or unless there's some reason why trait authors would need the ability to forbid clients from storing their return types in structs (is there one? I'm not aware of one).
Now what I actually wanted to say:
I think the RFC should address the publicly unnameable types issue. It should at least be an unresolved question, but if the author(s?) actually intended for this to make a typeof operator unnecessary, or less necessary, that should be made explicit. I have no strong opinion on whether we should in the long run add a typeof operator or rely on the proposed impl Trait type aliases or simply reject the idea that all returned types should be nameable in client code, but we shouldn't be committing to or ruling our any of those options by accident.
Readability of impl Trait type aliases
If we ignore interactions with other features for a moment, the only concern I have with this RFC in isolation is that it won't always be obvious what the concrete type of an associated impl type alias is intended to be. I'm fine with making the compiler do a limited form of module-level type inference (assuming the compiler team is confident it won't cause any problems), but there's a risk of also requiring every human who reads the code to do module-level type inference in their heads. If I were to start using this feature as currently proposed, I'd probably add the intended concrete type in a comment every time I wrote such an alias.
pub type Foo = impl Debug; // i32
It's hard to come up with a counter-proposal though. Adding the concrete type directly to the type alias statement feels bad because it breaks the principle that the signatures of a module's public items contain exactly what client code needs to know and no more (that is a thing in Rust, right? I'm not making that up?), and as cramertj explained "casting" or "ascribing" syntaxes would be pretty confusing here, but if we put the concrete type anywhere else that's not much of an improvement on requiring it to be explicit in every method's return values.
So at the moment, I think aturon's suggestion that the conservative implementation would be "each function using the alias, by itself, must contain enough information to fully infer the concrete type for the alias" seems like the best solution to this concern, since the human would probably only need to look at the first method after the associated impl type alias to figure out what the concrete type is. Consider this a vote in favor of "we should start with the most conservative approach and go from there".
|
My way-more-than-two cents on this RFC: I am strongly in favor of the functionality being proposed here.This makes it a lot easier to properly distinguish public APIs from implementation details in a way the compiler reliably enforces, makes it far more feasible to work with otherwise unnameable types like closures or messy "I really don't care what's in here" types like iterator combinators, and the syntax seems about as concise, obvious and ergonomic to me as it could possibly get. This seems very relevant to the "publicly unnameable types" issue, but the RFC never mentions that.I have no idea how widely known that issue is, or what everyone else is calling it these days, so I should probably explain what I mean by it: First, backstory: The only objection I'm aware of to having impl Trait be in the language at all is that it makes unnameable types more common. In particular, it causes types which were nameable within a function or module to become unnameable outside that function or module. I'm calling those "publicly unnameable types" to distinguish them from "everywhere unnameable types" like closures (which this RFC does mention). Note that being able to name a type does not mean being able to rely on that type never changing. For instance, if your library has a function returning i32, and my code puts that i32 in one of my structs, today I need to be able to write "i32" as part of my struct's type definition. If you change that i32 to impl Debug, then I can no longer rely on you always returning i32 anymore (which IS a good thing), but I also can't put your i32 in one of my structs anymore because Rust doesn't provide a way to say "whatever type that function returns" (which is NOT a good thing). Previously, I thought the only solution to this would be adding a typeof operator. Then I could write However, this RFC also alludes to a "sugar" for "impl Trait in traits", though it's never stated exactly what that is since it's not part of this proposal. I assume this hypothetical feature would mean making code like this:
be sugar for this:
If this sugar is added, then using it makes the type unnameable again. So there's an argument we should never actually add this sugar unless we also add something like typeof, or unless there's some reason why trait authors would need the ability to forbid clients from storing their return types in structs (is there one? I'm not aware of one). Now what I actually wanted to say: I think the RFC should address the publicly unnameable types issue. It should at least be an unresolved question, but if the author(s?) actually intended for this to make a typeof operator unnecessary, or less necessary, that should be made explicit. I have no strong opinion on whether we should in the long run add a typeof operator or rely on the proposed impl Trait type aliases or simply reject the idea that all returned types should be nameable in client code, but we shouldn't be committing to or ruling our any of those options by accident. Readability of impl Trait type aliasesIf we ignore interactions with other features for a moment, the only concern I have with this RFC in isolation is that it won't always be obvious what the concrete type of an associated impl type alias is intended to be. I'm fine with making the compiler do a limited form of module-level type inference (assuming the compiler team is confident it won't cause any problems), but there's a risk of also requiring every human who reads the code to do module-level type inference in their heads. If I were to start using this feature as currently proposed, I'd probably add the intended concrete type in a comment every time I wrote such an alias.
It's hard to come up with a counter-proposal though. Adding the concrete type directly to the type alias statement feels bad because it breaks the principle that the signatures of a module's public items contain exactly what client code needs to know and no more (that is a thing in Rust, right? I'm not making that up?), and as cramertj explained "casting" or "ascribing" syntaxes would be pretty confusing here, but if we put the concrete type anywhere else that's not much of an improvement on requiring it to be explicit in every method's return values. So at the moment, I think aturon's suggestion that the conservative implementation would be "each function using the alias, by itself, must contain enough information to fully infer the concrete type for the alias" seems like the best solution to this concern, since the human would probably only need to look at the first method after the associated impl type alias to figure out what the concrete type is. Consider this a vote in favor of "we should start with the most conservative approach and go from there". |
tomaka
referenced this pull request
Jul 22, 2017
Open
Allow explicitly expressing the type of a -> impl Trait #1738
glaebhoerl
referenced this pull request
in rust-lang/rust
Jul 25, 2017
Open
Tracking issue for RFC 1861: Extern types #43467
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Ericson2314
Jul 25, 2017
Contributor
What about full abstract type: bound = concrete;? #1951 assumes all the "rigorous phase" stuff would happen eventually, but IMO this is still beating around the bush with the impl shorthand.
|
What about full |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
cramertj
Jul 25, 2017
Member
@Ericson2314 I'm not sure I understand your proposal. With abstract <type>: <bound> = <concrete>;, you'd still have to specify the concrete type, which breaks the "unnameable types" use case. If you allow users to just write abstract <type>: <bound>; and have the concrete type inferred, that seems to be the same feature as type <type> = impl <bound>;.
Is your goal just to use a different syntax?
|
@Ericson2314 I'm not sure I understand your proposal. With Is your goal just to use a different syntax? |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
dlight
Jul 26, 2017
The type Item = impl Debug syntax is confusing, at least in the top level, because it isn't just a type alias: it also introduces a type equality constraint ensuring all uses of Item are the same concrete type. However, a top level type alias normally means that if you substituted it by its definition, the program would continue to work (for example, if you do type A = u32; then you can substitute all uses of A by u32).
So, I think that this should have another syntax, or at least this should be noted in the "drawbacks" section.
dlight
commented
Jul 26, 2017
|
The So, I think that this should have another syntax, or at least this should be noted in the "drawbacks" section. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
eddyb
Jul 26, 2017
Member
@dlight That interpretation is incorrect though. It's not a special syntax. Also, syntactical substitution is not guaranteed in Rust, and semantically, an impl Trait syntactical node has an identity (i.e. it's a declaration like struct Foo;) which it refers back to whenever mentioned.
The RFC probably needs more examples such as type Sequence = Vec<impl Element>;, or pairs, etc. to show that each impl Trait is independent from the type alias it happens to be declared in.
The only reason to write it as type Foo = impl Trait; most of the time is for interoperability but IMO typeof is better suited and easier to "implement" (it got left in the AST a while back, even if it's a syntax error or something, and most of the compiler ended up doing the right thing for it) - just needs an RFC now.
|
@dlight That interpretation is incorrect though. It's not a special syntax. Also, syntactical substitution is not guaranteed in Rust, and semantically, an The RFC probably needs more examples such as The only reason to write it as |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
dlight
Jul 26, 2017
In my mental model a type alias is just syntax sugar (not an associated type; just a top-level alias). Replacing aliases by their definition should never change whether a program typechecks. Could you point out some stable Rust code where my intuition is incorrect?
Anyway, even if it's incorrect, people may still be misled by it.
dlight
commented
Jul 26, 2017
|
In my mental model a type alias is just syntax sugar (not an associated type; just a top-level alias). Replacing aliases by their definition should never change whether a program typechecks. Could you point out some stable Rust code where my intuition is incorrect? Anyway, even if it's incorrect, people may still be misled by it. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
eddyb
Jul 26, 2017
Member
@dlight Path resolution is the most obvious one, since it's done in the scope of the definition.
Sadly "type blocks" isn't a thing otherwise type Foo = Vec<{ struct FooImpl; FooImpl }>; would be a decent example. You can put literally anything in the length of an array, e.g.:
type Bar = [(); {
// Any top level item, including nested modules.
mod foo {
// There is exactly one instance of this module.
pub struct Foo;
impl Foo {
pub const X: usize = 123;
}
}
foo::Foo::X
}];A more meaningful example, although not available on stable yet:
type Baz = [u8; { struct Quux(u8, u16); std::mem::size_of::<Quux>() }];If we had randomized field reordering, that one one would be guaranteed to always be the same type (i.e. its length would always be evaluated for the same struct Quux definition).
Of course these would make more sense with const generics, with the usize array length not being the only value you can have in any type, anymore.
Alright, so I don't have a perfect example. Still, a type alias has the same semantics as an associated type, once the latter has been resolved through the trait system, and they will likely grow even closer together. Having a syntactic alias would be limiting and wasteful.
There's another angle, I suppose - we can show that type alias expansion cannot be syntactic, given a definition such as this, that duplicates a type parameter:
type Double<T> = (T, T);Lifetime elision behaves independently of the expansion of Double (playpen):
fn elision_alias((x, _): Double<&str>) -> &str { x }
// error: "this function's return type contains a borrowed value, but the signature
// does not say which one of `(x, _)`'s 2 lifetimes it is borrowed from"
fn elision_syntax((x, _): (&str, &str)) -> &str { x }impl Trait (although not stable) has an identity that's not duplicated (playpen):
fn impl_trait_alias() -> Double<impl ToString> {
(String::new(), Default::default())
}
// error: "type annotations needed"
fn impl_trait_syntax() -> (impl ToString, impl ToString) {
// ^^^^^^^^^^^^^ cannot infer type for `_`
(String::new(), Default::default())
}|
@dlight Path resolution is the most obvious one, since it's done in the scope of the definition. type Bar = [(); {
// Any top level item, including nested modules.
mod foo {
// There is exactly one instance of this module.
pub struct Foo;
impl Foo {
pub const X: usize = 123;
}
}
foo::Foo::X
}];A more meaningful example, although not available on stable yet: type Baz = [u8; { struct Quux(u8, u16); std::mem::size_of::<Quux>() }];If we had randomized field reordering, that one one would be guaranteed to always be the same type (i.e. its length would always be evaluated for the same Of course these would make more sense with const generics, with the Alright, so I don't have a perfect example. Still, a There's another angle, I suppose - we can show that type Double<T> = (T, T);Lifetime elision behaves independently of the expansion of fn elision_alias((x, _): Double<&str>) -> &str { x }
// error: "this function's return type contains a borrowed value, but the signature
// does not say which one of `(x, _)`'s 2 lifetimes it is borrowed from"
fn elision_syntax((x, _): (&str, &str)) -> &str { x }
fn impl_trait_alias() -> Double<impl ToString> {
(String::new(), Default::default())
}
// error: "type annotations needed"
fn impl_trait_syntax() -> (impl ToString, impl ToString) {
// ^^^^^^^^^^^^^ cannot infer type for `_`
(String::new(), Default::default())
} |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
dlight
Jul 26, 2017
Thanks. Since type aliases aren't just syntactical already, it makes less sense to add different syntax just for being able to name a impl Trait. Also it seems cleaner / less ad-hoc than typeof (since you can give a meaningful name to the impl Trait, instead of referring to it indirectly).
I'm unable to find some kind of documentation for this subtle semantics around type aliases and their expansion. I looked on the first book, the second book, and the reference, but perhaps I should look at more advanced stuff (which I'm not sure exists yet?).
dlight
commented
Jul 26, 2017
•
|
Thanks. Since type aliases aren't just syntactical already, it makes less sense to add different syntax just for being able to name a I'm unable to find some kind of documentation for this subtle semantics around type aliases and their expansion. I looked on the first book, the second book, and the reference, but perhaps I should look at more advanced stuff (which I'm not sure exists yet?). |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
eddyb
Jul 26, 2017
Member
The issue is that there is no formal specification, otherwise I could link to that.
I would argue subtly surprising semantics would arise from a syntactic expansion (e.g. unhygienic macros), whereas the semantic expansion interprets the type where it is defined, just like field types in a struct definition, argument types in a function, etc.
There is a similar situation with const items: they are evaluated once (at the definition) and the value is (byte-wise) copied everywhere they're used, whereas a macro is far less rigorous, could expand to code with side-effects, etc.
Really, only macros should involve syntactic expansion and that's why they are invoked with a bang, i.e. ! after the macro name.
|
The issue is that there is no formal specification, otherwise I could link to that. There is a similar situation with Really, only macros should involve syntactic expansion and that's why they are invoked with a bang, i.e. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
dlight
Jul 27, 2017
Perhaps this RFC should mention this sketch of explicit existentials from RFC 1951 in the "Alternatives" section. I'm not sure if its semantics is a subset of this RFC (it looks like it is).
dlight
commented
Jul 27, 2017
|
Perhaps this RFC should mention this sketch of explicit existentials from RFC 1951 in the "Alternatives" section. I'm not sure if its semantics is a subset of this RFC (it looks like it is). |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
petrochenkov
Jul 29, 2017
Contributor
impl Trait in const and static
A logical extension of impl Trait in functions, and quite reasonable one.
Issue: The type of impl Trait in consts and statics is "revealed" in the current module, but the type of impl Trait in functions is not. They should behave identically, preferably "not revealed".
impl Trait in let
Kinda reasonable, by analogy with const/static, but I don't see enough motivation.
let bindings are usable only in the current module, if the type of impl Trait in let is revealed in the current module, like the RFC suggests, then it's always revealed, so what's the point.
Given that it also requires special rules for lifetimes to be usable, diverging from rules for other impl Trait, I'm not sure it worths it.
impl Trait in type aliases
type Alias = impl Trait; is a very counter-intuitive notation.
Even if I already know that the impl Trait has "identity" internally, it's still counter-intuitive.
Yes, after reading the @eddyb's long explanation it may look reasonable, but I'm not sure every person encountering this feature for the first time will go and read and understand it.
Can something similar to
type Alias: Trait; // No initializerbe used instead?
I'm also unhappy about module-level inference, as a human reader in particular.
Can a single use of Alias be marked as "canonical", so all the other uses could be forced to match it?
type Alias: Trait;
fn f1() -> marked_as_canonical Alias { ... }
fn f2() -> Alias { ... }And yeah, this example reminds me about typeof too, and can be reworded into
fn f1() -> impl Trait { ... }
type Alias = typeof(f1()); // f1 is "canonical"
fn f2() -> Alias { ... }Looks like not revealing the underlying type of Alias in the current module is impractical by definition, so it has to differ from impl Trait in functions in this respect, but I haven't looked into this deeper.
|
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
cramertj
Jul 31, 2017
Member
Issue: The type of impl Trait in consts and statics is "revealed" in the current module, but the type of impl Trait in functions is not. They should behave identically, preferably "not revealed".
My motivation for making consts and statics "leak" was to prevent users from having to sprinkle complex bounds in order to make unnameably-typed constants usable. However, I'm not sure how often this would be a problem in practice, and you could always work around it by using type Foo = impl Trait; const X: Foo = ...;. I'm definitely willing to be persuaded here, though I think my personal preference still tends towards allowing them to leak. Can you say more about why you'd prefer them to be private?
impl Trait in let...I'm not sure it worths it.
I think that it would be confusing to new users if we allow impl Trait in const and static but not let. It also just feels "wrong" to me from a more philosophical perspective.
WRT type alias syntax: there have been a lot of ideas floated in both this thread and others, so i'll try to briefly outline what I see as some of the main advantages and disadvantages of each proposed syntax:
abstype / abstract type Foo: I'm not super fond of the word "abstract" here. It doesn't seem like it adds much from a user's perspective (what does it mean for a type to be "abstract"?). However, it's a distinct keyword which makes it recognizable and easy to search for on the internet. I personally place a pretty high value on feature Google-ability, as it makes it much easier to discover and learn.type Foo = impl Trait;: this syntax seems straightforward and resembles existing type alias syntax. It's an obvious transition fromfn foo() -> impl Trait { ... }totype Foo = impl Trait; fn foo() -> Foo { ... }.type Foo: Trait;: I like that this separates the identity/assignment of the type from its declaration, which makes it clear that the type is being inferred. It could also allow things liketype Foo: Trait = MyStruct;which would allow users to keep the module-based abstraction boundary while still explicitly stating the concrete type. However, this could be "too much power"/too complex, and overall this syntax seems hard to Google for as it doesn't contain anyimpl Trait/abstype-esque special keyword.
I also gave some consideration to type Foo: impl Trait, which has the same advantages as type Foo: Trait while retaining Google-ability and a clear relationship to the impl Trait feature. However, this opens up all sorts of questions around "what the heck is type Foo: MyType = MyType;?" or similar.
Overall, my preference is towards type Foo = impl Trait;. I feel like it's the most natural syntax and will be the easiest for new users to recognize and make use of.
Can a single use of Alias be marked as "canonical", so all the other uses could be forced to match it?
This seems to me like an unnecessary complication. It's relatively easy for the compiler to determine if a function contains enough information to infer the concrete type, so I'd prefer to save users the extra work.
My motivation for making
I think that it would be confusing to new users if we allow WRT type alias syntax: there have been a lot of ideas floated in both this thread and others, so i'll try to briefly outline what I see as some of the main advantages and disadvantages of each proposed syntax:
I also gave some consideration to Overall, my preference is towards
This seems to me like an unnecessary complication. It's relatively easy for the compiler to determine if a function contains enough information to infer the concrete type, so I'd prefer to save users the extra work. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
eddyb
Jul 31, 2017
Member
FWIW eliding the type of a const/static should always be possible (#2010), except for i32 default for integer literals being unpopular (so it might be turned off if we accept that RFC), and for static items being potentially recursive (e.g. a circular linked list). Both cases can just use explicit types.
IMO that is a much better approach than impl Trait unless you want to limit the API surface of the value being placed in the global and/or if the type is a private implementation detail, e.g.:
struct MyAlloc {...}
pub static MY_ALLOC: impl Allocator = MyAlloc {...};|
FWIW eliding the type of a IMO that is a much better approach than struct MyAlloc {...}
pub static MY_ALLOC: impl Allocator = MyAlloc {...}; |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
glaebhoerl
Jul 31, 2017
Contributor
Would I be allowed to do things like:
type Foo = (impl Bar, impl Baz);or
type IterDisplay = impl Iterator<Item=impl Display>;?
If yes, that's a significant difference relative to the other two syntaxes, where you'd have to introduce a separate abstype (or whichever) by hand for each internal "impl Trait node" in the above definitions (corresponding to the fact that impl Trait is "side-effecting" and introduces new hidden items to the top-level scope, unlike plain type aliases, as discussed). For that matter: I can't remember whether or not the accepted impl Trait RFC for functions allows nested use like above; I assume that the same rule, whatever it may be, would apply to all of the positions where impl Trait is legal.
|
Would I be allowed to do things like: type Foo = (impl Bar, impl Baz);or type IterDisplay = impl Iterator<Item=impl Display>;? If yes, that's a significant difference relative to the other two syntaxes, where you'd have to introduce a separate |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
eddyb
Jul 31, 2017
Member
@glaebhoerl Those are allowed and I've previously mentioned the RFC should be more explicit on it.
They have always been part of my implementations, including nice things such as:
fn parse_csv<'a>(s: &'a str) -> impl Iterator<Item = impl Iterator<Item = &'a str>> {
s.split('\n').map(|line| line.split(','))
}|
@glaebhoerl Those are allowed and I've previously mentioned the RFC should be more explicit on it. fn parse_csv<'a>(s: &'a str) -> impl Iterator<Item = impl Iterator<Item = &'a str>> {
s.split('\n').map(|line| line.split(','))
} |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
petrochenkov
Jul 31, 2017
Contributor
My motivation for making consts and statics "leak" was to prevent users from having to sprinkle complex bounds in order to make unnameably-typed constants usable. However, I'm not sure how often this would be a problem in practice, and you could always work around it by using type Foo = impl Trait; const X: Foo = ...;. I'm definitely willing to be persuaded here, though I think my personal preference still tends towards allowing them to leak. Can you say more about why you'd prefer them to be private?
What makes impl Trait in functions different then? Why don't they suffer from the "complex bounds" problem? Constants are basically pure functions with no parameters.
I prefer the "not revealed" variant because it's consistent with functions and more conservative, if experience shows that it's impractical and causes boilerplate, it could be relaxed for both functions and constants.
What makes |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
cramertj
Jul 31, 2017
Member
@petrochenkov I agree that, when thinking about functions as const items, it makes more sense for their impl Trait visibility to match. I'll amend the RFC to change the visibility rules for consts, statics, and let unless anyone has a strong objection.
@glaebhoerl @eddyb Nested impl Trait is possible. I will add examples to the RFC to clarify that. However, I don't think this adds any additional expressiveness over the other syntax options: instead of type Foo = (impl Trait, impl Trait); one could write abstype X: Trait; abstype Y: Trait; type Foo = (X, Y); (though it is a pain, and it makes for a rough transition for users trying to refactor their impl Trait function signatures). One could also imagine something like type Foo: (impl Trait, impl Trait);, although as I said in my comment above, it's a bit weird to think about bounding a type with another type.
|
@petrochenkov I agree that, when thinking about functions as @glaebhoerl @eddyb Nested |
tomaka
referenced this pull request
in vulkano-rs/vulkano
Aug 1, 2017
Open
Changes after impl Trait type alias become stable #709
Manishearth
referenced this pull request
in request-for-explanation/podcast
Aug 4, 2017
Closed
#2071 impl Trait type alias #34
cramertj
added some commits
Aug 5, 2017
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
petrochenkov
Aug 5, 2017
Contributor
I wonder if it's possible to hide the underlying type of type Alias = impl Trait; in the current module as an initial experiment.
type Alias = impl Trait;
fn f() -> Alias { S } // OK
fn g() -> Alias { expr_returninng_S() } // OK
f().method_inherent_to_S(); // ERRORThe only places where the underlying type is revealed would be the "definition points" of impl Trait, i.e. the underlying type is revealed only during unification typeof(S) == underlying_typeof(Alias) && typeof(expr_returninng_S()) == underlying_typeof(Alias).
Is this technically possible?
The issue with revealed underlying types is that they open the door for inferring interfaces for all module items, which was avoided so far:
trait Anything {}
impl<T> Anything for T {}
type Infer1 = impl Anything;
//Add some macro hack to produce Infer1-InferN conveniently or even automatically with gensyms.
// We don't care about interfaces anymore! We infer everything!
fn f1() -> Infer1 { ... }
fn f2() -> Infer2 { ... }
fn f3() -> Infer3 { ... }
fn f4() -> Infer4 { ... }
fn f5() -> Infer5 { ... }
|
I wonder if it's possible to hide the underlying type of type Alias = impl Trait;
fn f() -> Alias { S } // OK
fn g() -> Alias { expr_returninng_S() } // OK
f().method_inherent_to_S(); // ERRORThe only places where the underlying type is revealed would be the "definition points" of The issue with revealed underlying types is that they open the door for inferring interfaces for all module items, which was avoided so far: trait Anything {}
impl<T> Anything for T {}
type Infer1 = impl Anything;
//Add some macro hack to produce Infer1-InferN conveniently or even automatically with gensyms.
// We don't care about interfaces anymore! We infer everything!
fn f1() -> Infer1 { ... }
fn f2() -> Infer2 { ... }
fn f3() -> Infer3 { ... }
fn f4() -> Infer4 { ... }
fn f5() -> Infer5 { ... }
|
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
glaebhoerl
Aug 5, 2017
Contributor
That does kind of suggest that
-
Explicitly specify the underlying type, and only hide its identity from outer modules, but it's revealed within the current module (a la
abstract type Foo: Trait = UnderlyingType;), and -
The underlying type gets inferred, but in exchange its identity is hidden even from you, and you only get access to the interface (
type Foo = impl Traitunder @petrochenkov's proposal)
should be two separate modes...
In other words, inferred types would always be hidden and specified types would always be "revealed".
(The particular thing that you can't write explicitly is the type of a closure, but that's fine because the Fn* interface is the only way you can access those anyways.)
|
That does kind of suggest that
should be two separate modes... In other words, inferred types would always be hidden and specified types would always be "revealed". (The particular thing that you can't write explicitly is the type of a closure, but that's fine because the |
text/0000-impl-trait-type-alias.md
| // Because we're outside `my_mod`, using a value of type `Foo` as anything | ||
| // other than `impl Debug` is an error: | ||
| let y: i32 = foo(); // ERROR: expected type `i32`, found type `Foo` |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
eddyb
Aug 5, 2017
Member
@petrochenkov @glaebhoerl I am against any inference outside/between functions and the RFC should reflect that position - otherwise I am afraid @cramertj and I have failed to communicate.
|
@petrochenkov @glaebhoerl I am against any inference outside/between functions and the RFC should reflect that position - otherwise I am afraid @cramertj and I have failed to communicate. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
glaebhoerl
Aug 5, 2017
Contributor
In that case, the important takeaway is that the difference between abstract type Foo: Trait = Type and type Foo = impl Trait is not just a question of syntax and convenience -- they do different things!
|
In that case, the important takeaway is that the difference between |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
msopena
May 11, 2018
After reading about all these due to the latest 1.26 release announcement, I have a bikeshed suggestion for the existential type syntax.
If I understood the discussion correctly, I personally don't like the abstract suggestion since I don't understood these types as being abstract (as per the meaning of the word). To me, they are concrete, but it's just that the user of them doesn't know the specific type name, so what about: hidden type Item: Debug;
msopena
commented
May 11, 2018
|
After reading about all these due to the latest 1.26 release announcement, I have a bikeshed suggestion for the If I understood the discussion correctly, I personally don't like the |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
alexreg
May 11, 2018
There's been lots of discussion about this already in the tracking issue. I'm with you on the abstract keyword, but I don't think hidden is a good/obvious/intuitive one either. Anyway, there have been other suggestions in that thread. :-)
alexreg
commented
May 11, 2018
|
There's been lots of discussion about this already in the tracking issue. I'm with you on the |
scottmcm
referenced this pull request
May 15, 2018
Closed
Escape Blocks and Labels as try/catch/throw control flow building blocks #2440
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Boscop
May 19, 2018
Why not use the impl keyword for existential types like this:
type Adder = impl Fn(usize) -> usize;
fn adder(a: usize) -> Adder {
|b| a + b
}
// existential type in associated type position:
struct MyType;
impl Iterator for MyType {
type Item = impl Debug;
fn next(&mut self) -> Option<Self::Item> {
Some("Another item!")
}
}
// `impl Trait` in `let`, `const`, and `static`:
const ADD_ONE: impl Fn(usize) -> usize = |x| x + 1;
static MAYBE_PRINT: Option<impl Fn(usize)> = Some(|x| println!("{}", x));
fn my_func() {
let iter: impl Iterator<Item = i32> = (0..5).map(|x| x * 5);
...
}This would make it more consistent.
But the problem is, what if we want to have an existential type without specifying any trait that it impls?
E.g. when having a function that returns an instance of an existential type with no trait constraint, and that type should be treated as an opaque type, and the return value of the function should be given as arg to another function from the same crate.
E.g. like this:
type Foo = impl;
fn f() -> Foo { 1 }
fn g(x: Foo) {}Could we just write impl with no trait to express this?
If not, why not use the shorter keyword some instead of the long existential?
Boscop
commented
May 19, 2018
•
|
Why not use the type Adder = impl Fn(usize) -> usize;
fn adder(a: usize) -> Adder {
|b| a + b
}
// existential type in associated type position:
struct MyType;
impl Iterator for MyType {
type Item = impl Debug;
fn next(&mut self) -> Option<Self::Item> {
Some("Another item!")
}
}
// `impl Trait` in `let`, `const`, and `static`:
const ADD_ONE: impl Fn(usize) -> usize = |x| x + 1;
static MAYBE_PRINT: Option<impl Fn(usize)> = Some(|x| println!("{}", x));
fn my_func() {
let iter: impl Iterator<Item = i32> = (0..5).map(|x| x * 5);
...
}This would make it more consistent. type Foo = impl;
fn f() -> Foo { 1 }
fn g(x: Foo) {}Could we just write |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
alexreg
May 19, 2018
@Boscop I think this is the exact syntax that I (and possibly others) proposed earlier in the thread. It's good, but there seems to be a vague consensus we should support the : bound syntax as well however, and favour it where possible.
Is there any use for an existential type with no constraint? Can this even be done for Box right now? i.e. Box<no constraint>?
alexreg
commented
May 19, 2018
|
@Boscop I think this is the exact syntax that I (and possibly others) proposed earlier in the thread. It's good, but there seems to be a vague consensus we should support the Is there any use for an existential type with no constraint? Can this even be done for |
pnkfelix
referenced this pull request
in rust-lang/rust
May 21, 2018
Closed
Add existential type syntax #50770
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Boscop
May 22, 2018
@alexreg Ah yes, I realized that there won't be a conflict with existing uses of type Foo: Trait because this syntax is only occupied already in trait decls, not trait impls and type decls outside of traits.
So I agree, I'd also favor the type Foo: Trait syntax for existential types.
But we have to make sure there won't be any conflicts with the use of this syntax in other situations in the future..
(As for existential type with no constraint, it can't be done with Box (unless you constrain it with Sized) but I guess if you need to have an existential type with no constraints (e.g. with a ref or pointer), you could write impl ?Sized..)
Boscop
commented
May 22, 2018
|
@alexreg Ah yes, I realized that there won't be a conflict with existing uses of (As for existential type with no constraint, it can't be done with Box (unless you constrain it with |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
alexreg
May 23, 2018
@Boscop But we don't have to worry even if those contexts overlap, because associated types are named existential types, in fact! And we'd want to support both syntaxes there. So I'm pretty sure there's no cause for concern here. :-)
Re Box with no constraint: that makes sense, thanks.
alexreg
commented
May 23, 2018
|
@Boscop But we don't have to worry even if those contexts overlap, because associated types are named existential types, in fact! And we'd want to support both syntaxes there. So I'm pretty sure there's no cause for concern here. :-) Re |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
hh9527
May 23, 2018
let type {
pub Foo impl Debug,
pub Bar impl Future<Item=Foo>,
} in fn foo() -> Bar {
future::ok("hello")
}does this make sense?
hh9527
commented
May 23, 2018
let type {
pub Foo impl Debug,
pub Bar impl Future<Item=Foo>,
} in fn foo() -> Bar {
future::ok("hello")
}does this make sense? |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
alexreg
commented
May 23, 2018
|
Not to me it doesn’t. Seems quite off-track... |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
varkor
Jun 18, 2018
Contributor
Maybe these concerns have already been stated, but I couldn't spot anything, so I thought it's better to restate them than omit them.
In the current designs (specific keyword/syntax aside), existential type seems far too inflexible as a desugaring for (return position / let type) impl Trait (which is what I gather is a point of existential type). Because it acts effectively as a separate keyword, you end up having to create separate standalone existential types when you want to use them within other types.
For example:
fn foo<T: Debug>(t: T) -> impl Debug { t }desugars to something like:
existential type Foo<T>: impl Debug;
fn foo<T: Debug>(t: T) -> Foo<T> { t }This is redundant and messy (you explicitly allow only a very constrained version of existential types, by placing the keyword before type).
Something like a keyword exists<T> [...] would be far more flexible and would remove these awkward abstractions. The previous example could be desugared:
fn foo<T: Debug>(t: T) -> exists<S: Debug> S { t }This would also allow existential types in any position (as far as I can tell, the current restriction isn't motivated by any technical problems with allowing them anywhere, as you can always simulate one by creating extra interim existential types).
I feel that if we go with the current design, the code generation is going to be awkward, and users lose flexibility for little reason.
(An exists<> construct would allow the suggested syntaxes like type X: Trait or type X = impl Trait just as well, without causing the same problems for things like return-position impl Trait.)
|
Maybe these concerns have already been stated, but I couldn't spot anything, so I thought it's better to restate them than omit them. In the current designs (specific keyword/syntax aside), For example: fn foo<T: Debug>(t: T) -> impl Debug { t }desugars to something like: existential type Foo<T>: impl Debug;
fn foo<T: Debug>(t: T) -> Foo<T> { t }This is redundant and messy (you explicitly allow only a very constrained version of existential types, by placing the keyword before Something like a keyword fn foo<T: Debug>(t: T) -> exists<S: Debug> S { t }This would also allow existential types in any position (as far as I can tell, the current restriction isn't motivated by any technical problems with allowing them anywhere, as you can always simulate one by creating extra interim I feel that if we go with the current design, the code generation is going to be awkward, and users lose flexibility for little reason. (An |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
alexreg
commented
Jun 18, 2018
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
varkor
Jun 18, 2018
Contributor
@alexreg: allowing things like (impl Foo, impl Bar) is better, but is still strictly less expressive than exists<>, as you can only express new existential types (you have no way of referring to the type itself). But maybe an adaption was proposed to account for this that I missed?
Additionally, from what I gathered, it seemed desirable that impl Trait was sugar for (some form of) existential types. Your proposed syntax desugars into exists<> nicely, but as far as I can tell would require two existential type declarations to express it.
|
@alexreg: allowing things like Additionally, from what I gathered, it seemed desirable that |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Ixrec
Jun 18, 2018
Contributor
Apparently we've lost some critical part of the context for this thread because I can't understand what the last few comments are even referring to.
@varkor What is fn foo<T>(t: T) -> impl Debug { t } or fn foo<T>(t: T) -> exists<S: Debug> S { t } is even trying to express? Does foo take any type T that implements Debug? (we already have fn foo<T: Debug> for that) Does foo take any type T that implicitly coerces to some type that implements Debug? (would we even want that???) Was the { t } supposed to be a placeholder and no implementation of the signature would actually look like that? In which case, did you mean "there is some type X that implements Debug, foo will somehow return it, and foo's argument can be any type T"? (which is easily done with existential, but only because T is not the return type; you seem to imply you want them to be the same type but that doesn't make any sense...) So, I can't even formulate a question about what this exists<> suggestion is supposed to do.
@alexreg What is "the syntax I proposed above and @Centril expanded upon."? I couldn't find a syntax proposal in any of your previous comments.
|
Apparently we've lost some critical part of the context for this thread because I can't understand what the last few comments are even referring to. @varkor What is @alexreg What is "the syntax I proposed above and @Centril expanded upon."? I couldn't find a syntax proposal in any of your previous comments. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
alexreg
commented
Jun 18, 2018
cramertj
deleted the
cramertj:impl-trait-alias
branch
Jun 18, 2018
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
varkor
Jun 18, 2018
Contributor
@lxrec: sorry, I've edited the comment to correct a bound. My main issue with the current syntax proposals is that they only allow existential qualifiers at the top level, which means you have to create additional existential types if you want to express types with nested existentials.
If the proposal is to allow type Foo: Bar and type Foo = impl Bar, then my concern is satisfied. But if a new keyword is proposed, then I think the design would need to be rethought.
|
@lxrec: sorry, I've edited the comment to correct a bound. My main issue with the current syntax proposals is that they only allow existential qualifiers at the top level, which means you have to create additional existential types if you want to express types with nested existentials. If the proposal is to allow |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Nemo157
Jun 18, 2018
Contributor
@varkor I don't think looking at this in terms of the desugaring of return position impl Trait is very useful, the fact that it may desugar through this more explicit form seems more targeted at language and compiler consistency than anything that should really affect the syntax of the more explicit form.
Instead I think it useful to look at how APIs that were previously written with impl Trait could instead switch to this form, and exactly what capabilities this form adds to those APIs.
Given your example of
pub fn foo<T: Debug>(t: T) -> impl Debug { t }pub existential type Foo<T>: impl Debug;
pub fn foo<T: Debug>(t: T) -> Foo<T> { t }pub fn foo<T: Debug>(t: T) -> <exists S: Debug> S { t }(I'm assuming the <exists> form in your original post is still missing the bound on T, otherwise I'm not sure how it could typecheck?)
The middle form is strictly more powerful than the outer two, it allows you to name the return type of foo outside the module
let f: bar::Foo<i32> = bar::foo(5);I guess you could put the <exists> form into a normal type alias
pub type Foo<T> = <exists S: Debug> S;
pub fn foo<T: Debug>(t: T) -> Foo<T> { t }but then you're basically back to the existential type form with a different syntax.
My main issue with the current syntax proposals is that they only allow existential qualifiers at the top level, which means you have to create additional existential types if you want to express types with nested existentials.
I assume by this that you intend to allow multiple bounds to be contained within one <exists> type? I briefly touched on this a few days ago in another context, and one thing I have come to realise is that it doesn't provided any additional capability over allowing anonymous existential types in an existential type declaration. As an example
pub fn foo() -> impl Iterator<Item = impl Display>;pub existential type Foo: impl Iterator<Item = impl Display>;
pub fn foo() -> Foo;pub type Foo = <exists T: Iterator, T::Item: Display> T;
pub fn foo() -> Foo;pub existential type Bar: impl Display;
pub existential type Foo: impl Iterator<Item = Bar>;
pub fn foo() -> Foo;The second and third forms are equivalently powerful, they still only allow you to name the direct return type of foo, the items that come out of the iterator are only nameable via their relationship
let f: Foo = foo();
let b: <Foo as Iterator>::Item = f.next().unwrap();whereas the fourth form does actually gives these items a name of their own. Whether that is useful in any specific instance will depend on the API.
|
@varkor I don't think looking at this in terms of the desugaring of return position Instead I think it useful to look at how APIs that were previously written with Given your example of pub fn foo<T: Debug>(t: T) -> impl Debug { t }pub existential type Foo<T>: impl Debug;
pub fn foo<T: Debug>(t: T) -> Foo<T> { t }pub fn foo<T: Debug>(t: T) -> <exists S: Debug> S { t }(I'm assuming the The middle form is strictly more powerful than the outer two, it allows you to name the return type of let f: bar::Foo<i32> = bar::foo(5);I guess you could put the pub type Foo<T> = <exists S: Debug> S;
pub fn foo<T: Debug>(t: T) -> Foo<T> { t }but then you're basically back to the
I assume by this that you intend to allow multiple bounds to be contained within one pub fn foo() -> impl Iterator<Item = impl Display>;pub existential type Foo: impl Iterator<Item = impl Display>;
pub fn foo() -> Foo;pub type Foo = <exists T: Iterator, T::Item: Display> T;
pub fn foo() -> Foo;pub existential type Bar: impl Display;
pub existential type Foo: impl Iterator<Item = Bar>;
pub fn foo() -> Foo;The second and third forms are equivalently powerful, they still only allow you to name the direct return type of let f: Foo = foo();
let b: <Foo as Iterator>::Item = f.next().unwrap();whereas the fourth form does actually gives these items a name of their own. Whether that is useful in any specific instance will depend on the API. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
varkor
Jun 18, 2018
Contributor
@Nemo157: yeah, there are two separate concerns here: the actual implementation and the syntax. I'll try to gather my thoughts more coherently later, but as a demonstration of how exists<> is more expressive than existential type or impl Trait as they currently stand, consider:
type Foo = exists<S: Bar, T: Baz> (S, S, T);This declares a triple such that the first two parameters are the same (hidden) type. To do this with existential type or impl Trait, you have to declare an additional type, because there's no way of specifying that the hidden types are the same (e.g. with (impl Bar, impl Bar, impl Baz)).
You should always be able to produce the same type, but you may have to declare multiple type aliases to achieve it without something to the effect of exists<> (if my made-up syntax makes sense).
|
@Nemo157: yeah, there are two separate concerns here: the actual implementation and the syntax. I'll try to gather my thoughts more coherently later, but as a demonstration of how type Foo = exists<S: Bar, T: Baz> (S, S, T);This declares a triple such that the first two parameters are the same (hidden) type. To do this with You should always be able to produce the same type, but you may have to declare multiple type aliases to achieve it without something to the effect of |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
alexreg
Jun 18, 2018
As @Centril pointed out on Discord, this is just HRTB/RankNTypes. It can (and should) be added at a later point. I still support the former consensus.
alexreg
commented
Jun 18, 2018
|
As @Centril pointed out on Discord, this is just HRTB/RankNTypes. It can (and should) be added at a later point. I still support the former consensus. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mikeyhew
Jun 29, 2018
I was just thinking about, and I see some other people have suggested, newtypes with an impl Trait field:
struct Foo(impl Trait);Is there any difference in expressivity between that and abstract type? If we could derive the Trait impl for Foo, is there an example of something that wouldn't work with the above syntax, but would work with abstract type?
mikeyhew
commented
Jun 29, 2018
|
I was just thinking about, and I see some other people have suggested, newtypes with an struct Foo(impl Trait);Is there any difference in expressivity between that and |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
varkor
Jun 29, 2018
Contributor
@mikeyhew: your Foo.0 is equivalent to the existential type/abstract type proposed here. As impl Trait is contextual, we want a standalone way to express existential types aside from impl Trait (which will likely be desugared into existential type or similar).
|
@mikeyhew: your |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Ericson2314
Jun 29, 2018
Contributor
@mikeyhew aside from some types having no name (closures), you don't even need the inner impl Trait. With the impl Trait you don't need the newtype and type Foo = impl Trait is fine.
|
@mikeyhew aside from some types having no name (closures), you don't even need the inner |
Centril
referenced this pull request
in Ericson2314/rust-rfcs
Jun 30, 2018
Closed
`extern existential type`s Pre-RFC so people can comment #2
Ericson2314
referenced this pull request
Jul 2, 2018
Open
RFC: Existential types with external definition #2492
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
alexreg
Jul 3, 2018
From discussion on Discord, it came up that we need to consider how this syntax would influence / contrast with the syntax for aliasing existential types. The latter I could see requiring trait aliasing, e.g.
trait D = A + B<C>;
type E = (i32, impl D, String);
// E == (i32, impl A + B<C>, String);I'm not sure if we'd want another syntax for this sort of thing, but either way I think it should inform the decision on syntax here. So does anyone have thought on how these two features should be kept distinct from a syntactical and conceptual point of view?
alexreg
commented
Jul 3, 2018
|
From discussion on Discord, it came up that we need to consider how this syntax would influence / contrast with the syntax for aliasing existential types. The latter I could see requiring trait aliasing, e.g. trait D = A + B<C>;
type E = (i32, impl D, String);
// E == (i32, impl A + B<C>, String);I'm not sure if we'd want another syntax for this sort of thing, but either way I think it should inform the decision on syntax here. So does anyone have thought on how these two features should be kept distinct from a syntactical and conceptual point of view? |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mikeyhew
Jul 3, 2018
@varkor I mean Foo itself, not its field. Maybe I should have used an explicit type for the field instead of impl Trait to make that clear (I used impl Trait because I was thinking the field would be a closure or generator).
I guess my question is, in a world where we could derive trait implementations for newtypes, what would the advantage of abstract type be? Is it just that you can avoid having to wrap/unwrap the value using the constructor, or is there something else that I am missing?
mikeyhew
commented
Jul 3, 2018
|
@varkor I mean I guess my question is, in a world where we could derive trait implementations for newtypes, what would the advantage of |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
varkor
Jul 3, 2018
Contributor
I wrote up some of the take-aways from an extensive discussion on existential types and impl Trait on the Discord rust-lang chat in the tracking issue, which might be of interest to those who aren't following along there.
|
I wrote up some of the take-aways from an extensive discussion on existential types and |
This was referenced Aug 5, 2018
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
varkor
Aug 5, 2018
Contributor
This RFC makes some invalid claims that I haven't seen disputed anywhere, but which really mess with the mental model I (and I know others) have for existential type.
Specifically, because the inferred inner type is visible within the module in which it is declared, it acts unlike impl Trait, which is always hidden, even within the module. Therefore, existential type cannot be used to desugar impl Trait in any form. They're completely distinct, albeit similar features.
The following statements in the RFC are at best misleading (arguably false):
Any such feature could, in the future, be added as essentially syntax sugar on top of this RFC, which is strictly more expressive.
It's not more expressive, because it doesn't allow the declaration of types hidden within the same module. The features referred to (such as impl Trait type aliases) can not be implemented as syntax sugar on top of the RFC.
By providing a separate syntax for "explicit" existential quantification,
impl Traitcan be taught as a syntactic sugar for generics and existential types.
Due to this problem impl Trait remains an entirely separate feature from existential type and thus cannot be taught in terms of synactic sugar.
Note that the current implementation of existential type in the compiler does not follow RFC 2071 when it comes to visibility. However, it does follow it in providing a desugaring for impl Trait.
The question now is whether existential type should be unified in behaviour with impl Trait, or we accept that these are two distinct features (and the implementation changed).
|
This RFC makes some invalid claims that I haven't seen disputed anywhere, but which really mess with the mental model I (and I know others) have for Specifically, because the inferred inner type is visible within the module in which it is declared, it acts unlike The following statements in the RFC are at best misleading (arguably false):
It's not more expressive, because it doesn't allow the declaration of types hidden within the same module. The features referred to (such as
Due to this problem Note that the current implementation of The question now is whether |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
alexreg
Aug 5, 2018
@varkor I don't quite understand this inconsistency. Maybe syntactic desugaring isn't possible, but semantically the difference just seems like one of scope to me.
alexreg
commented
Aug 5, 2018
|
@varkor I don't quite understand this inconsistency. Maybe syntactic desugaring isn't possible, but semantically the difference just seems like one of scope to me. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
varkor
Aug 5, 2018
Contributor
In this case, the issue is with whether the underlying type is transparent within the module in which it is declared, which is currently true for existential type but false for let-binding impl Trait. More details to follow.
|
In this case, the issue is with whether the underlying type is transparent within the module in which it is declared, which is currently true for |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
alexreg
Aug 6, 2018
@varkor I think the question is what you mean by "visible". Do you meant transparent? Because neither is, as far as I know. Or do you mean nameable? I don't know what "visible" means, really.
alexreg
commented
Aug 6, 2018
|
@varkor I think the question is what you mean by "visible". Do you meant transparent? Because neither is, as far as I know. Or do you mean nameable? I don't know what "visible" means, really. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
rpjohnst
Aug 6, 2018
@alexreg Take a look at the examples in the RFC, e.g. fn get_larger_foo. You can write code that relies on things about the underlying type beyond the trait it's declared to implement, as long as you're in the same module.
(The current implementation does not allow this; the RFC proposes it.)
rpjohnst
commented
Aug 6, 2018
|
@alexreg Take a look at the examples in the RFC, e.g. (The current implementation does not allow this; the RFC proposes it.) |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
alexreg
Aug 6, 2018
@rpjohnst Right. That's what I understood by "transparent type". In terms of resolving the inconsistency here, I'm more in favour of keeping the behaviour of the current implementation (even if that means doing an FCP or new RFC).
alexreg
commented
Aug 6, 2018
|
@rpjohnst Right. That's what I understood by "transparent type". In terms of resolving the inconsistency here, I'm more in favour of keeping the behaviour of the current implementation (even if that means doing an FCP or new RFC). |
cramertj commentedJul 20, 2017
•
edited
Edited 1 time
-
cramertj
edited Sep 18, 2017 (most recent)
Add the ability to create named existential types and support impl Trait in let, const, and static declarations.
Rendered