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

Named existentials and impl Trait variable declarations #2071

Merged
merged 14 commits into from Sep 18, 2017

Conversation

@cramertj
Member

cramertj commented Jul 20, 2017

Add the ability to create named existential types and support impl Trait in let, const, and static declarations.

// existential types
existential type Adder: Fn(usize) -> usize;
fn adder(a: usize) -> Adder {
    |b| a + b
}

// existential type in associated type position:
struct MyType;
impl Iterator for MyType {
    existential type Item: 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);
    ...
}

Rendered

@aturon aturon added the T-lang label Jul 20, 2017

@aturon aturon self-assigned this Jul 20, 2017

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

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.

Member

aturon commented Jul 20, 2017

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.

Show outdated Hide outdated 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.

@Ixrec

Ixrec Jul 20, 2017

Contributor

Is this : meant to be a =?

@Ixrec

Ixrec Jul 20, 2017

Contributor

Is this : meant to be a =?

This comment has been minimized.

@cramertj

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

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.

Show outdated Hide outdated text/0000-impl-trait-type-alias.md
inner: T
};
type Foo<T> -> impl Debug;

This comment has been minimized.

@Ixrec

Ixrec Jul 20, 2017

Contributor

Similarly, is this -> meant to be a =?

@Ixrec

Ixrec Jul 20, 2017

Contributor

Similarly, is this -> meant to be a =?

@scottmcm

This comment has been minimized.

Show comment
Hide comment
@scottmcm

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?

Member

scottmcm commented Jul 21, 2017

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?

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

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];
Member

eddyb commented Jul 21, 2017

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];
@cramertj

This comment has been minimized.

Show comment
Hide comment
@cramertj

cramertj Jul 21, 2017

Member

@scottmcm

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.

Member

cramertj commented Jul 21, 2017

@scottmcm

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.

@cramertj

This comment has been minimized.

Show comment
Hide comment
@cramertj

cramertj Jul 21, 2017

Member

@scottmcm

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: "" } 
}
Member

cramertj commented Jul 21, 2017

@scottmcm

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: "" } 
}
@Ixrec

This comment has been minimized.

Show comment
Hide comment
@Ixrec

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".

Contributor

Ixrec commented Jul 22, 2017

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".

@glaebhoerl glaebhoerl referenced this pull request in rust-lang/rust Jul 25, 2017

Open

Tracking issue for RFC 1861: Extern types #43467

1 of 3 tasks complete
@Ericson2314

This comment has been minimized.

Show comment
Hide comment
@Ericson2314

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.

Contributor

Ericson2314 commented Jul 25, 2017

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.

@cramertj

This comment has been minimized.

Show comment
Hide comment
@cramertj

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?

Member

cramertj commented Jul 25, 2017

@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?

@dlight

This comment has been minimized.

Show comment
Hide comment
@dlight

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

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

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.

Member

eddyb commented Jul 26, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@dlight

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.

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

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())
}
Member

eddyb commented Jul 26, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@dlight

dlight Jul 26, 2017

@eddyb

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

@eddyb

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?).

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

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.

Member

eddyb commented Jul 26, 2017

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.

@dlight

This comment has been minimized.

Show comment
Hide comment
@dlight

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).

@petrochenkov

This comment has been minimized.

Show comment
Hide comment
@petrochenkov

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 initializer

be 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.

Contributor

petrochenkov commented Jul 29, 2017

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 initializer

be 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.

@cramertj

This comment has been minimized.

Show comment
Hide comment
@cramertj

cramertj Jul 31, 2017

Member

@petrochenkov

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 from fn foo() -> impl Trait { ... } to type 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 like type 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 any impl 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.

Member

cramertj commented Jul 31, 2017

@petrochenkov

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 from fn foo() -> impl Trait { ... } to type 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 like type 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 any impl 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.

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

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 {...};
Member

eddyb commented Jul 31, 2017

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 {...};
@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

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.

Contributor

glaebhoerl commented Jul 31, 2017

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.

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

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(','))
}
Member

eddyb commented Jul 31, 2017

@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(','))
}
@petrochenkov

This comment has been minimized.

Show comment
Hide comment
@petrochenkov

petrochenkov Jul 31, 2017

Contributor

@cramertj

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.

Contributor

petrochenkov commented Jul 31, 2017

@cramertj

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.

@cramertj

This comment has been minimized.

Show comment
Hide comment
@cramertj

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.

Member

cramertj commented Jul 31, 2017

@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.

@tomaka tomaka referenced this pull request in vulkano-rs/vulkano Aug 1, 2017

Open

Changes after impl Trait type alias become stable #709

@Manishearth Manishearth referenced this pull request in request-for-explanation/podcast Aug 4, 2017

Closed

#2071 impl Trait type alias #34

@petrochenkov

This comment has been minimized.

Show comment
Hide comment
@petrochenkov

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(); // ERROR

The 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 { ... }
Contributor

petrochenkov commented Aug 5, 2017

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(); // ERROR

The 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 { ... }
@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Aug 5, 2017

Contributor

That does kind of suggest that

  1. 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

  2. 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 Trait under @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.)

Contributor

glaebhoerl commented Aug 5, 2017

That does kind of suggest that

  1. 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

  2. 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 Trait under @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.)

Show outdated Hide outdated 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.

@istankovic

istankovic Aug 5, 2017

Shouldn't this be my_mod::foo()?

@istankovic

istankovic Aug 5, 2017

Shouldn't this be my_mod::foo()?

This comment has been minimized.

@cramertj

cramertj Aug 5, 2017

Member

Yes! Thank you.

@cramertj

cramertj Aug 5, 2017

Member

Yes! Thank you.

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

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.

Member

eddyb commented Aug 5, 2017

@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.

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

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!

Contributor

glaebhoerl commented Aug 5, 2017

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!

@msopena

This comment has been minimized.

Show comment
Hide comment
@msopena

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

@alexreg

This comment has been minimized.

Show comment
Hide comment
@alexreg

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 abstract keyword, but I don't think hidden is a good/obvious/intuitive one either. Anyway, there have been other suggestions in that thread. :-)

@Boscop

This comment has been minimized.

Show comment
Hide comment
@Boscop

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

@alexreg

This comment has been minimized.

Show comment
Hide comment
@alexreg

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 : 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>?

@pnkfelix pnkfelix referenced this pull request in rust-lang/rust May 21, 2018

Closed

Add existential type syntax #50770

@Boscop

This comment has been minimized.

Show comment
Hide comment
@Boscop

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 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..)

@alexreg

This comment has been minimized.

Show comment
Hide comment
@alexreg

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 Box with no constraint: that makes sense, thanks.

@hh9527

This comment has been minimized.

Show comment
Hide comment
@hh9527

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?

@alexreg

This comment has been minimized.

Show comment
Hide comment
@alexreg

alexreg May 23, 2018

Not to me it doesn’t. Seems quite off-track...

alexreg commented May 23, 2018

Not to me it doesn’t. Seems quite off-track...

@varkor

This comment has been minimized.

Show comment
Hide comment
@varkor

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.)

Contributor

varkor commented Jun 18, 2018

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.)

@alexreg

This comment has been minimized.

Show comment
Hide comment
@alexreg

alexreg Jun 18, 2018

@varkor I think you missed the syntax I proposed above and @Centril expanded upon. We would permit e.g. (impl Foo, impl Bar) as a type, alongside the trait bound syntax for existentials (which would be preferred where possible).

alexreg commented Jun 18, 2018

@varkor I think you missed the syntax I proposed above and @Centril expanded upon. We would permit e.g. (impl Foo, impl Bar) as a type, alongside the trait bound syntax for existentials (which would be preferred where possible).

@varkor

This comment has been minimized.

Show comment
Hide comment
@varkor

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.

Contributor

varkor commented Jun 18, 2018

@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.

@Ixrec

This comment has been minimized.

Show comment
Hide comment
@Ixrec

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.

Contributor

Ixrec commented Jun 18, 2018

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.

@alexreg

This comment has been minimized.

Show comment
Hide comment
@alexreg

alexreg Jun 18, 2018

@Ixrec It's in the tracking issue for this actually. That's where this discussion should be taking place in fact.

@varkor Maybe we could move this discussion back to the tracking issue where a lot of it has already taken place? :-)

alexreg commented Jun 18, 2018

@Ixrec It's in the tracking issue for this actually. That's where this discussion should be taking place in fact.

@varkor Maybe we could move this discussion back to the tracking issue where a lot of it has already taken place? :-)

@cramertj cramertj deleted the cramertj:impl-trait-alias branch Jun 18, 2018

@varkor

This comment has been minimized.

Show comment
Hide comment
@varkor

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.

Contributor

varkor commented Jun 18, 2018

@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.

@Nemo157

This comment has been minimized.

Show comment
Hide comment
@Nemo157

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.

Contributor

Nemo157 commented Jun 18, 2018

@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

This comment has been minimized.

Show comment
Hide comment
@varkor

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).

Contributor

varkor commented Jun 18, 2018

@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).

@alexreg

This comment has been minimized.

Show comment
Hide comment
@alexreg

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.

@mikeyhew

This comment has been minimized.

Show comment
Hide comment
@mikeyhew

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?

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?

@varkor

This comment has been minimized.

Show comment
Hide comment
@varkor

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).

Contributor

varkor commented Jun 29, 2018

@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).

@Ericson2314

This comment has been minimized.

Show comment
Hide comment
@Ericson2314

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.

Contributor

Ericson2314 commented Jun 29, 2018

@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.

@alexreg

This comment has been minimized.

Show comment
Hide comment
@alexreg

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?

@mikeyhew

This comment has been minimized.

Show comment
Hide comment
@mikeyhew

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

@varkor

This comment has been minimized.

Show comment
Hide comment
@varkor

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.

Contributor

varkor commented Jul 3, 2018

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.

@varkor

This comment has been minimized.

Show comment
Hide comment
@varkor

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 Trait can 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).

cc @cramertj and @oli-obk

Contributor

varkor commented Aug 5, 2018

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 Trait can 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).

cc @cramertj and @oli-obk

@alexreg

This comment has been minimized.

Show comment
Hide comment
@alexreg

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.

@varkor

This comment has been minimized.

Show comment
Hide comment
@varkor

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.

Contributor

varkor commented Aug 5, 2018

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.

@alexreg

This comment has been minimized.

Show comment
Hide comment
@alexreg

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.

@rpjohnst

This comment has been minimized.

Show comment
Hide comment
@rpjohnst

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. 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.)

@alexreg

This comment has been minimized.

Show comment
Hide comment
@alexreg

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).

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