Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Named existentials and impl Trait variable declarations #2071

Merged
merged 14 commits into from Sep 18, 2017

Conversation

@cramertj
Copy link
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.

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

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

This comment has been minimized.

@cramertj

cramertj Jul 21, 2017

Author Member

Yes-- same for the other one. Thanks for catching that. I'll fix it as soon as I get to a computer.

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

@scottmcm

This comment has been minimized.

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

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

Copy link
Member Author

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.

Copy link
Member Author

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.

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

@Ericson2314

This comment has been minimized.

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

Copy link
Member Author

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.

Copy link

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.

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

Copy link

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.

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

Copy link

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.

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

Copy link

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.

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

Copy link
Member Author

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.

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

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

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

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

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Sep 12, 2018

@alexreg we need an RFC for that.

@cramertj

This comment has been minimized.

Copy link
Member Author

cramertj commented Sep 12, 2018

@alexreg If you're planning to write one, you should read mine first and let me know what you think / what you would change.

@alexreg

This comment has been minimized.

Copy link

alexreg commented Sep 12, 2018

@cramertj Ah cool. Is there a PR for it yet? I'll give you feedback soon. :-)

@cramertj

This comment has been minimized.

Copy link
Member Author

cramertj commented Sep 12, 2018

@alexreg No, I wrote it more than a year ago and left it unopened because I think it makes some unfortunate tradeoffs (inferred associated types in particular) in order to get consistently-nameable types. I'm not sure that's something we want to do, and I wanted to get more experience using named existentials in practice before we went down that road.

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Sep 12, 2018

@cramertj read it; looks good overall -- may need some polish textually. Quick notes:

  1. The APIT part is already done since trait Foo { fn bar(x: impl Baz); } is legal.

  2. associated type inference seems orthogonal and is likely controversial. It also likely has interesting interactions with associated type defaults (e.g. #2532). I'd suggest leaving that out and starting with something more conservative.

I'm interested in collaborating with you and @alexreg on this if you are game.

@cramertj

This comment has been minimized.

Copy link
Member Author

cramertj commented Sep 12, 2018

@Centril

associated type inference seems orthogonal

Nah, it's not orthogonal at all-- it's the whole feature. it's the only reason you can write -> impl Trait and wind up with an associated type that you can name.

@cramertj

This comment has been minimized.

Copy link
Member Author

cramertj commented Sep 12, 2018

The big issue that comes up again and again with RPIT in traits is the ability to name the return type. Associated type inference is the mechanism by which the RFC solves that problem, but it's definitely controversial and unfortunate.

The APIT part is already done since trait Foo { fn bar(x: impl Baz); } is legal.

Yeah, note that I wrote this RFC long before the impl-trait-in-arg-position feature was implemented, at which point we didn't consider it part of the RFC. it was something that got approved later via an FCP during the implementation process.

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Sep 12, 2018

@cramertj

Nah, it's not orthogonal at all-- it's the whole feature. it's the only reason you can write -> impl Trait and wind up with an associated type that you can name.

I'm suggesting that initially, you shouldn't be able to wind up with an associated type that you can name and that the only way to name the type returned is via existential type.

E.g. we initially only allow:

trait Foo {
    fn bar<...>(...) -> impl Baz; // optional default definition...
}

impl Foo for MyType {
    // You are not allowed to specify anything but `-> impl Baz` here.
    fn bar<...>(...) -> impl Baz { .. }
}

As you noted in the RFC, this is limiting in particular wrt. existing traits and "mixing and matching"; but it is a start.

What my main point is that associated type inference and the above is separable as features and can be introduced separately without future-compat issues.

Associated type inference is the mechanism by which the RFC solves that problem, but it's definitely controversial and unfortunate.

I only agree that it is controversial; I don't necessarily agree it is unfortunate -- in fact it seems like it could be a big boon for ergonomics for Iterator and friends (Strategy, Parser, Future, ...); but it can be proposed on its own merits as an extension.

Yeah, note that I wrote this RFC long before the impl-trait-in-arg-position feature was implemented, ...

I inferred as much :)

@cramertj

This comment has been minimized.

Copy link
Member Author

cramertj commented Sep 12, 2018

@Centril

we initially only allow
trait Foo {
fn bar<...>(...) -> impl Baz; // optional default definition...
}

Ah, see, that's the issue-- I don't know if we ever want that, because having the return type of the method be completely unnameable means that there's no way to impose additional bounds on it, something which is needed in many if not most use-cases for impl Trait (e.g. Send bounds). You can make it "renameable" again by introducing something like typeof(Foo::bar)::Output or outputof(Foo::bar), but I prefer approaches which allow us to name the values. One option that has been presented before was to create a matching associated type with the same name as the method that returned -> impl Trait (e.g. in this case outputof(Foo::bar) would just be spelled Foo::bar or Foo::Bar if we did re-casing). However, this only works for top-level impl Trait types, and not for e.g. -> Option<impl Trait> or -> Result<impl Trait, impl Trait> etc.

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Sep 13, 2018

@cramertj

I don't know if we ever want that,

And by this you mean that you don't know if we ever only want that?
(since your RFC allowed what I previously described)

because having the return type of the method be completely unnameable [..]

It's not completely unnameable; you can write:

type NameIt = impl Baz;

fn stuff(..) -> NameIt {
    <MyType as Foo>::bar(..)
}

You can make it "renameable" again

There's always (#2524):

type Foo = _;

which affords all operations that the underlying type does.


I think there are 4 distinct features here:

(F1) impl Trait in trait definitions.

trait Alan {
    type Turing: Alonzo<Church = impl Kurt>;
    // ^ this should be equivalent to writing:
    type Turing: Alonzo<Church: Kurt>;
    // ^ per RFC 2289
    // ^ or equivalently:  trait Alan where Self::Turing: Alonzo<Church: Kurt>

    const Gödel: impl Haskell;
    // ^ Gödel is optionally generic (GATs).

    fn curry<'william>(...) -> impl 'william + Howard;
}

Using impl Trait in these positions denotes the minimal bound which you may assume simply by having T: Alan.

(F2) impl Trait in return positions in trait implementations:

impl Bob for Atkey {
    type Conor = impl McBride; // RFC 2071 + 2515

    const PHILIP: impl Wadler = 42;

    fn thierry() -> impl Coquand { .. }
}

Using impl Trait inside a trait implementation denotes the maximal bound (+ auto trait leakage) which you may assume when using <Bob as Atkey>::thierry() and does not necessarily have to coincide with what is specified in the minimal bound (F1) but must at least satisfy the minimal bound.

(F3) Letting trait implementations to have a type T where impl Bound is required in return positions where T: Bound holds but where T != impl Bound. This allows you to write things like:

trait Stephanie {}
struct Weirich;
impl Stephanie for Weirich {}

trait Ulf {
    fn norell() -> impl Stephanie;
}

struct Emily;

impl Ulf for Emily {
    fn norell() -> Weirich { Weirich }
}

And now you may assume that <Emily for Ulf>::norell == Weirich.

(F4) Associated type inference. E.g.:

struct Riehl;

impl Iterator for Riehl {
    fn next(&mut self) -> Option<EugeniaCheng> { ... }
}

The dependency graphs are roughly:

F1 -> F2 or F3 (only need one of F2 or F3 for F1 to be useful)
F2 -> F1 or F4 (only need one of them for F2 to be meaningful)
F3 -> F1 or F4 (ditto)
F4 -> []

We could thus for example do F1, F2, F3 without doing F4.
We could start with F4 as it depends on nothing else.
But we could also do all of it in one go. I'm up any option really.

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Sep 13, 2018

Further thought: The value of F4 is more clearly shown when used in conjunction F1-F3 but it is useful on its own.

Also: there are more places in which impl Trait could show up:

  1. impl Itertools for impl Iterator { .. } is equivalent to impl<_I: Iterator> Itertools for _I { .. }
  2. impl<T: Foo<Assoc = impl Bar>> Baz for Quux<T> { .. } equivalent to: impl<_B: Bar, T: Foo<Assoc = _B>> Baz for Quux<T> { .. }

which is appealing if you are in the consistency/uniformity-extermism school of language design ;)

@cramertj

This comment has been minimized.

Copy link
Member Author

cramertj commented Sep 13, 2018

@Centril

And by this you mean that you don't know if we ever only want that?

No, I'm unsure about if we ever want to support -> impl Trait in trait definitions, which can't be named in bounds. I think I only want to support it if we have a good story around naming the type. e.g.:

trait Polygon { ... }
trait Rectangle { ... }
struct Square { ... }
impl Rectangle for Square { ... }

trait IntoPolygon {
    fn into_polygon(self) -> impl Polygon;
}

fn do_something_with_rectangleable<T: IntoPolygon>(x: T) where T::???: Rectangle {
    let rectangle: impl Rectangle = x.into_polygon();
    ...
}

In my experience, most traits with -> impl Trait functions have needs for bounds like the above, which is inexpressible without additional features.

@Boscop

This comment has been minimized.

Copy link

Boscop commented Sep 16, 2018

@cramertj But there are trait methods that don't have the needs for bounds like that, just like free functions that only promise to return a impl Polygon without a way to check if it's also a Rectangle.
When defining a trait, one decides for which methods the caller should be able to behave differently depending on which actual type is returned.
For those methods where the caller should be able to check bounds on the return type, an assoc type will be introduced.
When we have impl Trait for assoc types, it could be written like this to allow trait bounds checks:

trait IntoPolygon {
    type Poly = impl Polygon;
    fn into_polygon(self) -> Self::Poly;
}

Assoc types are defined by each impl of a trait. The actual return type of trait methods returning impl Trait is defined by the caller (method body) so it's also defined by each impl of a trait. So you could argue that we should just have impl Trait for assoc types and we don't really need it for return types of trait methods.
But in cases where there is no use for doing any trait bound checks on the return type, it would be more concise (less verbose) if we don't have to introduce an assoc type for the return type, if it's not used anywhere else, e.g. when returning impl Iterator, where the caller will never need/want to know the actual return type.
E.g. it would be more concise to write

trait Foo {
    fn bar() -> impl Iterator<Item = i32>;
}

than

trait Foo {
    type BarReturnType = impl Iterator<Item = i32>;
    fn bar() -> Self::BarReturnType;
}

if that type is never used in any checks, and not intended to be checked.

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Sep 16, 2018

@Boscop Why not... just... type Poly: Polygon;? Which works today?
Then type Poly = impl Polyon;, or some other syntax in the impl.

@cramertj

This comment has been minimized.

Copy link
Member Author

cramertj commented Sep 17, 2018

@Boscop

if that type is never used in any checks, and not intended to be checked.

It's this bit that I think is an antipattern. Needing to name a return type like this or place bounds on one is very common. I would advise everyone, certainly library authors, never to use a feature that made traits with unnameable return types. I don't think we should add features to the language that we then advocate against using.

@Boscop

This comment has been minimized.

Copy link

Boscop commented Oct 13, 2018

I often have the situation that I want to write extension traits, e.g.:

image

I wish it would just work like that..

@Boscop

This comment has been minimized.

Copy link

Boscop commented Oct 13, 2018

Hm, it doesn't even work with existential type:

error[E0700]: hidden type for `impl Trait` captures lifetime that does not appear in bounds
  --> src/main.rs:13:2
   |
13 |     existential type R: Iterator<Item = (A, &'a mut T)> /*+ 'a*/;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
note: hidden type `std::iter::FilterMap<I, [closure@src/main.rs:15:19: 15:54]>` captures the lifetime 'a as defined on the impl at 11:6
  --> src/main.rs:11:6
   |
11 | impl<'a, A, T: 'a, I: Iterator<Item = (A, &'a mut Option<T>)> /*+ 'a*/> FilterMap2<'a, A, T> for I {
   |      ^^

error: aborting due to previous error

https://play.rust-lang.org/?gist=b13f589b6f8e7813a689e98319bed0f5&version=nightly&mode=debug&edition=2015

So what's the right way to do this? :)

@earthengine

This comment has been minimized.

Copy link

earthengine commented Oct 16, 2018

@cramertj

It's this bit that I think is an antipattern.

Can you please give a concrete example to show this is not good? IMO removing the ability to name the type and making constraint on it is not a bad thing.

It clearly tells the code reader: "Here is a type, you should only use its Trait ability, no to assume anything else about it", this relaxes the code flexibility - the code writer can adjust the return type freely, without the fear of breaking backward compatibility.

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Nov 3, 2018

@Boscop Huh, I would think that would work, because 'a shows up in the Iterator bound.
But even with Captures<'a> it doesn't work. cc @oli-obk

(also, shouldn't this be posted on the tracking issue?)

hcpl added a commit to hcpl/rust.vim that referenced this pull request Nov 24, 2018

Add new keywords
* `async` from rust-lang/rfcs#2394;
* `existential` from rust-lang/rfcs#2071.

@hcpl hcpl referenced this pull request Nov 24, 2018

Merged

Add new keywords #282

da-x added a commit to rust-lang/rust.vim that referenced this pull request Nov 29, 2018

Add new keywords (#282)
* Add new keywords

* `async` from rust-lang/rfcs#2394;
* `existential` from rust-lang/rfcs#2071.

* Make `existential` a contextual keyword

Thanks @dlrobertson who let me use his PR
#284!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.