RFC: associated items and multidispatch #195

Merged
merged 5 commits into from Sep 16, 2014

Conversation

Projects
None yet
@aturon
Member

aturon commented Aug 12, 2014

This RFC extends traits with associated items, which make generic programming
more convenient, scalable, and powerful. In particular, traits will consist of a
set of methods, together with:

  • Associated functions (already present as "static" functions)
  • Associated statics
  • Associated types
  • Associated lifetimes

These additions make it much easier to group together a set of related types,
functions, and constants into a single package.

This RFC also provides a mechanism for multidispatch traits, where the impl
is selected based on multiple types. The connection to associated items will
become clear in the detailed text below.

Rendered view

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 13, 2014

Member

cc @glaebhoerl, this is somewhat along the lines of your multidispatch alternative.

Member

aturon commented Aug 13, 2014

cc @glaebhoerl, this is somewhat along the lines of your multidispatch alternative.

@pcwalton

This comment has been minimized.

Show comment
Hide comment
@pcwalton

pcwalton Aug 13, 2014

Contributor

Note that much of this is implemented and waiting for review: rust-lang/rust#16377

Contributor

pcwalton commented Aug 13, 2014

Note that much of this is implemented and waiting for review: rust-lang/rust#16377

@Gankro

This comment has been minimized.

Show comment
Hide comment
@Gankro

Gankro Aug 13, 2014

Contributor

+100000, this is a massive ergonomics and sanity win.

If a trait implementor overrides any default associated types, they must also override all default functions and methods.

I love the simplicity of this, but I can't help but wonder if there's a middle-ground to be had. Have you considered only invalidating the defaults that depend on the associated types that were actually changed? For instance (might butcher the precise syntax here):

trait Foo {
    type A = int;
    type B = bool;
    fn handleA() -> A { ... }
    fn handleB() -> B { ... }
}

impl Foo for Bar {
   type A = uint;
   fn handleA() -> A { ... } // have to reimplement
   // but handleB is still valid
}

I could see this getting complicated to resolve though, since you would need to check if handleB could result in the calling of a function that depends on A. I also don't know how often you would have a case where this is actually useful. It could reduce code copy-pasting, though.

It's also worth noting that trait-level where clauses are never needed for constraining associated types anyway, because associated types also have where clauses.

What about Where clauses that depend on multiple associated types? Surely those should be Trait-level bounds, and not on the individual types? e.g.

trait Foo
    where Bar<Self::Input, Self::Output>: Encodable {

    type Output;
    type Input;
    ...
}

Or am I misunderstanding? (Regardless, I agree that it could be very confusing to use types before they're "declared").

Contributor

Gankro commented Aug 13, 2014

+100000, this is a massive ergonomics and sanity win.

If a trait implementor overrides any default associated types, they must also override all default functions and methods.

I love the simplicity of this, but I can't help but wonder if there's a middle-ground to be had. Have you considered only invalidating the defaults that depend on the associated types that were actually changed? For instance (might butcher the precise syntax here):

trait Foo {
    type A = int;
    type B = bool;
    fn handleA() -> A { ... }
    fn handleB() -> B { ... }
}

impl Foo for Bar {
   type A = uint;
   fn handleA() -> A { ... } // have to reimplement
   // but handleB is still valid
}

I could see this getting complicated to resolve though, since you would need to check if handleB could result in the calling of a function that depends on A. I also don't know how often you would have a case where this is actually useful. It could reduce code copy-pasting, though.

It's also worth noting that trait-level where clauses are never needed for constraining associated types anyway, because associated types also have where clauses.

What about Where clauses that depend on multiple associated types? Surely those should be Trait-level bounds, and not on the individual types? e.g.

trait Foo
    where Bar<Self::Input, Self::Output>: Encodable {

    type Output;
    type Input;
    ...
}

Or am I misunderstanding? (Regardless, I agree that it could be very confusing to use types before they're "declared").

@sfackler

This comment has been minimized.

Show comment
Hide comment
@sfackler

sfackler Aug 13, 2014

Member

I was also a bit worried about the clause @Gankro pointed out, but I don't think we can sanely do any better. Any rule that would involve poking around in the internals of the default implementations seems kind of unworkable, as there's no indication to the programmer as to what's going on, and makes it too easy to accidentally break backwards compatibility during a refactor of internal implementation details.

Member

sfackler commented Aug 13, 2014

I was also a bit worried about the clause @Gankro pointed out, but I don't think we can sanely do any better. Any rule that would involve poking around in the internals of the default implementations seems kind of unworkable, as there's no indication to the programmer as to what's going on, and makes it too easy to accidentally break backwards compatibility during a refactor of internal implementation details.

@sfackler

This comment has been minimized.

Show comment
Hide comment
@sfackler

sfackler Aug 13, 2014

Member

I also don't really think that it'll come up very often, since it doesn't seem like default associated types will be a very common pattern.

👍 to the overall RFC

Member

sfackler commented Aug 13, 2014

I also don't really think that it'll come up very often, since it doesn't seem like default associated types will be a very common pattern.

👍 to the overall RFC

+ ... <existing productions>
+ | 'static' IDENT ':' TYPE [ '=' CONST_EXP ] ';'
+ | 'type' IDENT [ ':' BOUNDS ] [ WHERE_CLAUSE ] [ '=' TYPE ] ';'
+ | 'lifetime' LIFETIME_IDENT ';'

This comment has been minimized.

@huonw

huonw Aug 13, 2014

Member

This requires adding a keyword?

@huonw

huonw Aug 13, 2014

Member

This requires adding a keyword?

This comment has been minimized.

@aturon

aturon Aug 13, 2014

Member

Yes. I'll add a note about that.

@aturon

aturon Aug 13, 2014

Member

Yes. I'll add a note about that.

+ }
+}
+
+impl<K,V> HashMap<K,V> where K: ContainerKey {

This comment has been minimized.

@huonw

huonw Aug 13, 2014

Member

(Just to be 100% clear, is this different to impl<K: ContainerKey, V> HashMap<K, V>?)

@huonw

huonw Aug 13, 2014

Member

(Just to be 100% clear, is this different to impl<K: ContainerKey, V> HashMap<K, V>?)

This comment has been minimized.

@aturon

aturon Aug 13, 2014

Member

They are the same.

@aturon

aturon Aug 13, 2014

Member

They are the same.

+ // as implemented by `Vec<T>`
+```
+
+### Ways to reference items

This comment has been minimized.

@huonw

huonw Aug 13, 2014

Member

I don't see any mention of associated lifetimes here (nor in the grammar/example paths above).

@huonw

huonw Aug 13, 2014

Member

I don't see any mention of associated lifetimes here (nor in the grammar/example paths above).

This comment has been minimized.

@aturon

aturon Aug 13, 2014

Member

I believe they should work identically to type paths. I'll add details to the RFC soon.

@aturon

aturon Aug 13, 2014

Member

I believe they should work identically to type paths. I'll add details to the RFC soon.

active/0000-associated-items.md
+}
+```
+
+Note that `Vec<T>::E` and `Vec<T>::empty` are also valid type and function

This comment has been minimized.

@huonw

huonw Aug 13, 2014

Member

Should the latter be Vec::<T>::empty? (The only context in which one can use a function reference is an expression?)

@huonw

huonw Aug 13, 2014

Member

Should the latter be Vec::<T>::empty? (The only context in which one can use a function reference is an expression?)

@bvssvni

This comment has been minimized.

Show comment
Hide comment
@bvssvni

bvssvni Aug 13, 2014

+1 There are many places in the gamedev libraries where this will improve ergonomics. It will also make it easier to generalize traits and add default behavior, because you can add new associated types with default. It will reduce the clutter with extra generic parameters, for example <B: BackEnd<I: ImageSize>, I> becomes <B: BackEnd>.

The non-clashing of parameters  and that you can pass in a named output type is nice. There are cases where it is not obvious what the generic parameter should be or why it is there, where this syntax will help readability in either way.

In cases where multiple bounds share the name of an associated type, it should be a helpful error message. Example fn baz<G: Foo + Bar>(g: &G) -> G::A where A is an associated type with both Foo and Bar. Rust can then suggest "Did you mean <G as Foo>::A or <G as Bar>::A?".

Some questions:

  • Can an associated static be default by using the default type of another associated type? type A = int; static ZERO: A = 0;?
  • Will it be possible to override associated statics?

A few things:

  •  not_allowed(U: Foo) should be not_allowed<U: Foo>
  • There is an example without Rust syntax (search for 'consume_foo')

bvssvni commented Aug 13, 2014

+1 There are many places in the gamedev libraries where this will improve ergonomics. It will also make it easier to generalize traits and add default behavior, because you can add new associated types with default. It will reduce the clutter with extra generic parameters, for example <B: BackEnd<I: ImageSize>, I> becomes <B: BackEnd>.

The non-clashing of parameters  and that you can pass in a named output type is nice. There are cases where it is not obvious what the generic parameter should be or why it is there, where this syntax will help readability in either way.

In cases where multiple bounds share the name of an associated type, it should be a helpful error message. Example fn baz<G: Foo + Bar>(g: &G) -> G::A where A is an associated type with both Foo and Bar. Rust can then suggest "Did you mean <G as Foo>::A or <G as Bar>::A?".

Some questions:

  • Can an associated static be default by using the default type of another associated type? type A = int; static ZERO: A = 0;?
  • Will it be possible to override associated statics?

A few things:

  •  not_allowed(U: Foo) should be not_allowed<U: Foo>
  • There is an example without Rust syntax (search for 'consume_foo')
active/0000-associated-items.md
+ println!("{}", u.as_T())
+}
+
+fn not_allowed(U: Foo)(u: U) {

This comment has been minimized.

@bvssvni

bvssvni Aug 13, 2014

This should be not_allowed<U: Foo>(u: U).

@bvssvni

bvssvni Aug 13, 2014

This should be not_allowed<U: Foo>(u: U).

active/0000-associated-items.md
+associated types or lifetimes (but do have to specify the input types), trait
+object types must specify all of the types:
+
+```

This comment has been minimized.

@bvssvni

bvssvni Aug 13, 2014

Add Rust colored syntax.

@bvssvni

bvssvni Aug 13, 2014

Add Rust colored syntax.

active/0000-associated-items.md
+
+The proposed scoping rule is:
+
+* Associated types are in scope for the trait body

This comment has been minimized.

@huonw

huonw Aug 13, 2014

Member

How does this interact with nested items?

E.g.

trait Foo {
    type T;

    fn method(&self) {
        fn nested() -> T { ... }
    }
}

Specifically, is the T in scope for nested? If so, this differs to any other generic parameters; if not, this differs to a non-associated type would be in scope there. (It presumably should be the former, since they're generics at heart.)

@huonw

huonw Aug 13, 2014

Member

How does this interact with nested items?

E.g.

trait Foo {
    type T;

    fn method(&self) {
        fn nested() -> T { ... }
    }
}

Specifically, is the T in scope for nested? If so, this differs to any other generic parameters; if not, this differs to a non-associated type would be in scope there. (It presumably should be the former, since they're generics at heart.)

+Note that output constraints are allowed when referencing a trait in a *type* or
+a *bound*, but not in an `IMPL_SEGMENT` path:
+
+* As a type: `fn foo(obj: Box<Iterator<A = uint>>` is allowed.

This comment has been minimized.

@huonw

huonw Aug 13, 2014

Member

It seems these constraints are the only way to write trait objects that contain output types?

e: I commented before reading the section below.

@huonw

huonw Aug 13, 2014

Member

It seems these constraints are the only way to write trait objects that contain output types?

e: I commented before reading the section below.

active/0000-associated-items.md
+ let f = Foo { ... };
+ let b = Bar { ... };
+ let mut v = Vec::new();
+ v.push(box f as Show);

This comment has been minimized.

@huonw

huonw Aug 13, 2014

Member

as Box<Show>?

@huonw

huonw Aug 13, 2014

Member

as Box<Show>?

@Kimundi

This comment has been minimized.

Show comment
Hide comment
@Kimundi

Kimundi Aug 13, 2014

Member

+1 from me, this makes generic functions so much better looking in many cases.

Member

Kimundi commented Aug 13, 2014

+1 from me, this makes generic functions so much better looking in many cases.

@nrc

This comment has been minimized.

Show comment
Hide comment
@nrc

nrc Aug 13, 2014

Member

Looks good! My biggest question is what will happen with sub-traits - in particular, if a super trait declares associated items, are these inherited by the sub-trait? Can they be overridden? Can the bounds be specialised (as in virtual types)? I suspect we just want the simplest solution here, but I'm not sure exactly what that is.

My mental model for type params in traits is that there are two factors - 'input' vs 'output' wrt to impl selection and internal vs external in terms of the API. The node and edge types (as in the RFC) are internal whereas the type of items in a Vec is external. I think these two properties are mostly orthogonal. The two are conflated in the RFC. Could you give some examples of traits with 'external' type params and how they would work? E.g., Vec. I assume you would keep the type param as a type param, not as assoc type, but you don't want it to affect impl selection, is that an issue or not?

Do you have examples of where associated lifetimes are useful? I don't think I've seen these motivated before.

I found the scoping rules a bit odd at first, but I think I agree that what you suggest is the best solution. I found it hard to reason that we require self.m(), but not Self::T. I guess I should not try to equate the receiver with the static type.

I'm a bit uncomfortable that we can constrain associated types using bounds, or where clauses on the trait, or where clauses on the assoc type. It seems like there ought to be one place to look not three. I guess if we move to only where clauses, that will get rid of bounds on assoc types too? I like requiring Self:: in the trait where clauses - that seems to indicate that the programmer should prefer local where clauses.

I find the shorthand for equality constraints a bit out of place - I assume you can't give actual type parameters for assoc types when using a trait, so it seems weird that you can specify constraints as if you could. I think that is a type is really internal then it should not be necessary to constrain it externally. I.e., this is a breach of encapsulation. Do you have an example of where it does make sense and/or is necessary?

Which brings me to trait objects. I find this part of the proposal is getting really complex. Is there a simpler solution, like not allowing trait objects for traits with assoc types? I worry that up to this point, assoc types are very static and clearly resolvable, whereas for trait objects, we rely a lot on types which are not known at compile time. Firstly this is complicated, and secondly I worry about soundness edge cases here (although perhaps with minimal sub-traits stuff this won't be a problem). From an encapsulation perspective, it seems that anything outside the object should not have to care about the assoc types. From the perspective of trait objects, I would prefer that assoc items are treated like Self and 'erased' and thus can't be exposed out of the object. But I realise, this would severely restrict the usefulness of assoc types in general. Do you have examples of where trait objects with the exposed assoc types are useful/necessary?

Is it correct that inherent assoc types are just scoped type aliases? Are these allowed today? And will this RFC change things here? If there is a change, do you have some motivation for inherant assoc types?

I prefer the tuple approach to multi-dispatch. Given that it is rare, I don't mind too much that it feels a little bit bolted on. Could you expand the first disadvantage please? I don't see what you mean there. It seems like if we went for the tuple approach we could then allow type parameters to be external, output params and be more backwards compatible and solve some of the complex edge cases mentioned above.

Member

nrc commented Aug 13, 2014

Looks good! My biggest question is what will happen with sub-traits - in particular, if a super trait declares associated items, are these inherited by the sub-trait? Can they be overridden? Can the bounds be specialised (as in virtual types)? I suspect we just want the simplest solution here, but I'm not sure exactly what that is.

My mental model for type params in traits is that there are two factors - 'input' vs 'output' wrt to impl selection and internal vs external in terms of the API. The node and edge types (as in the RFC) are internal whereas the type of items in a Vec is external. I think these two properties are mostly orthogonal. The two are conflated in the RFC. Could you give some examples of traits with 'external' type params and how they would work? E.g., Vec. I assume you would keep the type param as a type param, not as assoc type, but you don't want it to affect impl selection, is that an issue or not?

Do you have examples of where associated lifetimes are useful? I don't think I've seen these motivated before.

I found the scoping rules a bit odd at first, but I think I agree that what you suggest is the best solution. I found it hard to reason that we require self.m(), but not Self::T. I guess I should not try to equate the receiver with the static type.

I'm a bit uncomfortable that we can constrain associated types using bounds, or where clauses on the trait, or where clauses on the assoc type. It seems like there ought to be one place to look not three. I guess if we move to only where clauses, that will get rid of bounds on assoc types too? I like requiring Self:: in the trait where clauses - that seems to indicate that the programmer should prefer local where clauses.

I find the shorthand for equality constraints a bit out of place - I assume you can't give actual type parameters for assoc types when using a trait, so it seems weird that you can specify constraints as if you could. I think that is a type is really internal then it should not be necessary to constrain it externally. I.e., this is a breach of encapsulation. Do you have an example of where it does make sense and/or is necessary?

Which brings me to trait objects. I find this part of the proposal is getting really complex. Is there a simpler solution, like not allowing trait objects for traits with assoc types? I worry that up to this point, assoc types are very static and clearly resolvable, whereas for trait objects, we rely a lot on types which are not known at compile time. Firstly this is complicated, and secondly I worry about soundness edge cases here (although perhaps with minimal sub-traits stuff this won't be a problem). From an encapsulation perspective, it seems that anything outside the object should not have to care about the assoc types. From the perspective of trait objects, I would prefer that assoc items are treated like Self and 'erased' and thus can't be exposed out of the object. But I realise, this would severely restrict the usefulness of assoc types in general. Do you have examples of where trait objects with the exposed assoc types are useful/necessary?

Is it correct that inherent assoc types are just scoped type aliases? Are these allowed today? And will this RFC change things here? If there is a change, do you have some motivation for inherant assoc types?

I prefer the tuple approach to multi-dispatch. Given that it is rare, I don't mind too much that it feels a little bit bolted on. Could you expand the first disadvantage please? I don't see what you mean there. It seems like if we went for the tuple approach we could then allow type parameters to be external, output params and be more backwards compatible and solve some of the complex edge cases mentioned above.

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Aug 13, 2014

Contributor

Looks basically good to me! Comments:

First of all, GHC has a very similar system of associated types which has been refined over several iterations. It would probably be wise to study it for inspiration, to avoid repeating mistakes, and also to repeat avoiding mistakes (less cute phrasing: to also avoid making the mistakes which they've consciously avoided making). The relevant section in the manual might be a good starting point, but it might also be worthwhile to simply send an email to their mailing list for any advice or background they might have: they would probably be happy to help out.

In particular I think any point where we deviate from GHC's behavior likely deserves heightened scrutiny. (Thinking here mainly of type system related, rather than merely syntactic aspects.)

Some specific points:

  • The RFC only talks about associated typedefs/aliases/synonyms, but GHC also has associated datatypes (and in fact had them first). The difference is that associated type aliases are defined to refer to existing types, while associated datatypes always create brand new types. This is important partly because associated datatypes are injective (T::A = U::A => T = U), while associated type aliases aren't, and partly because associated datatypes are themselves first-class types which you can e.g. write trait impls for, while associated type aliases aren't. See for example this stackoverflow question.

    (If we wanted to match what GHC has, the Rust syntax would likely be trait Foo { enum D; }, impl Foo for Bar { enum D { A, B } }, impl Foo for Baz { enum D { Quux(int) } }, etc. -- it's important for each instance (impl) to have its own set of constructors (variants), like data in Haskell does, which is only possible with enums in Rust, not structs. On the positive side, this would be a narrowly avoided opportunity for syntax bikeshedding.)

  • It should be noted that GHC doesn't allow attaching constraints (where clauses) directly to type aliases, associated or otherwise. (I'm not familiar with the specific reasons, but suspect there might be good ones.)

  • It's possible to use an auxiliary trait to "recover" input type syntax for an associated output type:

    trait Iterator {
        type A;
        fn next(&mut self) -> Option<A>;
    }
    
    trait IteratorOf<T>: Iterator where Self::A = T { }
    impl<Iter: Iterator> IteratorOf<Iter::A> for Iter { }
    
    fn sum_uints<Iter: IteratorOf<uint>>(iter: Iter) -> uint { ... }
    
    // does the same thing as:
    fn sum_uints<Iter: Iterator<A = uint>>(iter: Iter) -> uint { ... }
    

    This might be an alternative to the Iterator<A = foo> sugar, though the sugar is likely still preferable to having to write these additional boilerplate traits.

  • In the future, we may wish to relax the "overlapping instances" rule so that one can provide "blanket" trait implementations and then "specialize" them for particular types. [...]

    GHC forbids overlapping type family instances, even with the OverlappingInstances language extension (feature gate), for basically the same reasons cited (it violates soundness). (Though with newer extensions like ConstraintKinds, I think even OverlappingInstances on its own is sufficient to violate soundness... in any case, I think we should resist overlapping impls as hard as we can, and even if we do ever allow them, should consider them unsafe.)

  • All trait items (including methods, understood as UFCS functions) are in scope for the trait body.

    This option seems overly aggressive: there is little benefit to writing some_method(self, arg) rather than self.some_method(arg), and the UFCS version loses autoderef etc.

    Perhaps there's little benefit, but is there a known drawback?

  • With FlexibleInstances in GHC (something which Rust has natively), it's possible to create overlapping instances (impls) without using OverlappingInstances or defining any orphan instances. We should test whether we're vulnerable to the same issue (and if not, why not). (Note that a module in Haskell corresponds to a crate in Rust.)

Contributor

glaebhoerl commented Aug 13, 2014

Looks basically good to me! Comments:

First of all, GHC has a very similar system of associated types which has been refined over several iterations. It would probably be wise to study it for inspiration, to avoid repeating mistakes, and also to repeat avoiding mistakes (less cute phrasing: to also avoid making the mistakes which they've consciously avoided making). The relevant section in the manual might be a good starting point, but it might also be worthwhile to simply send an email to their mailing list for any advice or background they might have: they would probably be happy to help out.

In particular I think any point where we deviate from GHC's behavior likely deserves heightened scrutiny. (Thinking here mainly of type system related, rather than merely syntactic aspects.)

Some specific points:

  • The RFC only talks about associated typedefs/aliases/synonyms, but GHC also has associated datatypes (and in fact had them first). The difference is that associated type aliases are defined to refer to existing types, while associated datatypes always create brand new types. This is important partly because associated datatypes are injective (T::A = U::A => T = U), while associated type aliases aren't, and partly because associated datatypes are themselves first-class types which you can e.g. write trait impls for, while associated type aliases aren't. See for example this stackoverflow question.

    (If we wanted to match what GHC has, the Rust syntax would likely be trait Foo { enum D; }, impl Foo for Bar { enum D { A, B } }, impl Foo for Baz { enum D { Quux(int) } }, etc. -- it's important for each instance (impl) to have its own set of constructors (variants), like data in Haskell does, which is only possible with enums in Rust, not structs. On the positive side, this would be a narrowly avoided opportunity for syntax bikeshedding.)

  • It should be noted that GHC doesn't allow attaching constraints (where clauses) directly to type aliases, associated or otherwise. (I'm not familiar with the specific reasons, but suspect there might be good ones.)

  • It's possible to use an auxiliary trait to "recover" input type syntax for an associated output type:

    trait Iterator {
        type A;
        fn next(&mut self) -> Option<A>;
    }
    
    trait IteratorOf<T>: Iterator where Self::A = T { }
    impl<Iter: Iterator> IteratorOf<Iter::A> for Iter { }
    
    fn sum_uints<Iter: IteratorOf<uint>>(iter: Iter) -> uint { ... }
    
    // does the same thing as:
    fn sum_uints<Iter: Iterator<A = uint>>(iter: Iter) -> uint { ... }
    

    This might be an alternative to the Iterator<A = foo> sugar, though the sugar is likely still preferable to having to write these additional boilerplate traits.

  • In the future, we may wish to relax the "overlapping instances" rule so that one can provide "blanket" trait implementations and then "specialize" them for particular types. [...]

    GHC forbids overlapping type family instances, even with the OverlappingInstances language extension (feature gate), for basically the same reasons cited (it violates soundness). (Though with newer extensions like ConstraintKinds, I think even OverlappingInstances on its own is sufficient to violate soundness... in any case, I think we should resist overlapping impls as hard as we can, and even if we do ever allow them, should consider them unsafe.)

  • All trait items (including methods, understood as UFCS functions) are in scope for the trait body.

    This option seems overly aggressive: there is little benefit to writing some_method(self, arg) rather than self.some_method(arg), and the UFCS version loses autoderef etc.

    Perhaps there's little benefit, but is there a known drawback?

  • With FlexibleInstances in GHC (something which Rust has natively), it's possible to create overlapping instances (impls) without using OverlappingInstances or defining any orphan instances. We should test whether we're vulnerable to the same issue (and if not, why not). (Note that a module in Haskell corresponds to a crate in Rust.)

active/0000-associated-items.md
+INPUT_PARAM = IDENT [ ':' BOUNDS ]
+
+BOUNDS = IDENT { '+' IDENT }* [ '+' ]
+BOUND = IDENT [ '<' ARGS '>' ]

This comment has been minimized.

@ben0x539

ben0x539 Aug 13, 2014

This should be BOUNDS = BOUND { '+' BOUND }* [ '+' ]?

@ben0x539

ben0x539 Aug 13, 2014

This should be BOUNDS = BOUND { '+' BOUND }* [ '+' ]?

@ben0x539

This comment has been minimized.

Show comment
Hide comment
@ben0x539

ben0x539 Aug 13, 2014

Would where-clauses on associated types allow a trait like

trait Add {
    type LHS;
    type RHS where (LHS, RHS) = Self;
    type SUM;
    fn add(&LHS, &RHS) -> SUM;
}

that can be implemented mostly like the "Multidispatch through tuple types" alternative example?

Would where-clauses on associated types allow a trait like

trait Add {
    type LHS;
    type RHS where (LHS, RHS) = Self;
    type SUM;
    fn add(&LHS, &RHS) -> SUM;
}

that can be implemented mostly like the "Multidispatch through tuple types" alternative example?

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 13, 2014

Member

@Gankro

If a trait implementor overrides any default associated types, they must also override all default functions and methods.

I love the simplicity of this, but I can't help but wonder if there's a middle-ground to be had. Have you considered only invalidating the defaults that depend on the associated types that were actually changed?

The problem, as @sfackler points out, is that the dependency might not show up in the function/method signature -- it might only show up in the implementation. It seems like a bad idea for the overriding rules to be dependent on the details of those implementations. I'll revise the RFC to clarify this point.

Member

aturon commented Aug 13, 2014

@Gankro

If a trait implementor overrides any default associated types, they must also override all default functions and methods.

I love the simplicity of this, but I can't help but wonder if there's a middle-ground to be had. Have you considered only invalidating the defaults that depend on the associated types that were actually changed?

The problem, as @sfackler points out, is that the dependency might not show up in the function/method signature -- it might only show up in the implementation. It seems like a bad idea for the overriding rules to be dependent on the details of those implementations. I'll revise the RFC to clarify this point.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 13, 2014

Member

@Gankro

What about Where clauses that depend on multiple associated types? Surely those should be Trait-level bounds, and not on the individual types? e.g.

trait Foo
    where Bar<Self::Input, Self::Output>: Encodable {

    type Output;
    type Input;
    ...
}

It's a bit less natural, but you can place this constraint on either of the associated types:

trait Foo {
    type Output;
    type Input where Bar<Input, Output>: Encodable;
    ...
}
Member

aturon commented Aug 13, 2014

@Gankro

What about Where clauses that depend on multiple associated types? Surely those should be Trait-level bounds, and not on the individual types? e.g.

trait Foo
    where Bar<Self::Input, Self::Output>: Encodable {

    type Output;
    type Input;
    ...
}

It's a bit less natural, but you can place this constraint on either of the associated types:

trait Foo {
    type Output;
    type Input where Bar<Input, Output>: Encodable;
    ...
}
@Gankro

This comment has been minimized.

Show comment
Hide comment
@Gankro

Gankro Aug 13, 2014

Contributor

So the where clauses on a trait are basically just a bunch of random claims that must all be satisfied? Is this valid?

trait Foo {
    type Output;
    type Input where Output: Show;
    ...
}

Should it be?

Contributor

Gankro commented Aug 13, 2014

So the where clauses on a trait are basically just a bunch of random claims that must all be satisfied? Is this valid?

trait Foo {
    type Output;
    type Input where Output: Show;
    ...
}

Should it be?

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 13, 2014

Member

@Gankro under the current design, yes, that would be valid. A similar question came up in the where clause RFC.

Currently, the where clause RFC allows arbitrary bounds, but this was mainly to support the multidispatch encoding. Since this RFC provides a more direct means of multidispatch, we could instead have a rule like: a where clause must mention at least one of the type parameters bound by the item it is constraining.

That rule would allow my example, but forbid yours.

Member

aturon commented Aug 13, 2014

@Gankro under the current design, yes, that would be valid. A similar question came up in the where clause RFC.

Currently, the where clause RFC allows arbitrary bounds, but this was mainly to support the multidispatch encoding. Since this RFC provides a more direct means of multidispatch, we could instead have a rule like: a where clause must mention at least one of the type parameters bound by the item it is constraining.

That rule would allow my example, but forbid yours.

@ben0x539

This comment has been minimized.

Show comment
Hide comment
@ben0x539

ben0x539 Aug 13, 2014

The grammar for where-claused associated types type <ident> [<where-clause>] [= <default>] seems pretty confusing, if you have type Foo where <blahblah> = X; you need to inspect the <blahblah> to figure out whether it's a complete T: U bound so that X is the default type for Foo or whether it's just another type T so that the complete bound is actually T = X and there's no default type.

edit: Oh, they're spelled where T == X? I got confused by the Apply example, I suppose :(

The grammar for where-claused associated types type <ident> [<where-clause>] [= <default>] seems pretty confusing, if you have type Foo where <blahblah> = X; you need to inspect the <blahblah> to figure out whether it's a complete T: U bound so that X is the default type for Foo or whether it's just another type T so that the complete bound is actually T = X and there's no default type.

edit: Oh, they're spelled where T == X? I got confused by the Apply example, I suppose :(

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 13, 2014

Member

@nick29581

Looks good! My biggest question is what will happen with sub-traits - in particular, if a super trait declares associated items, are these inherited by the sub-trait? Can they be overridden? Can the bounds be specialised (as in virtual types)? I suspect we just want the simplest solution here, but I'm not sure exactly what that is.

This is an excellent point: I completely overlooked subtraits. I will work out a design and update the RFC.

My mental model for type params in traits is that there are two factors - 'input' vs 'output' wrt to impl selection and internal vs external in terms of the API. The node and edge types (as in the RFC) are internal whereas the type of items in a Vec is external. I think these two properties are mostly orthogonal. The two are conflated in the RFC. Could you give some examples of traits with 'external' type params and how they would work? E.g., Vec. I assume you would keep the type param as a type param, not as assoc type, but you don't want it to affect impl selection, is that an issue or not?

I don't understand the internal/external distinction. For the type Vec, you want a type parameter which can be instantiated in various ways. But for e.g. Container traits, the element type is always an output. For any given concrete Self type like Vec<u8> the element type is determined: it must be u8. And if you want to request a container with a given element type, you'd used Container<A=u8>. But I may be missing your point, here.

Do you have examples of where associated lifetimes are useful? I don't think I've seen these motivated before.

Basically any trait that today would take a lifetime argument would instead have an associated lifetime. Such traits are rare, but they do come up. I'll add examples/motivation to the RFC.

I'm a bit uncomfortable that we can constrain associated types using bounds, or where clauses on the trait, or where clauses on the assoc type. It seems like there ought to be one place to look not three. I guess if we move to only where clauses, that will get rid of bounds on assoc types too? I like requiring Self:: in the trait where clauses - that seems to indicate that the programmer should prefer local where clauses.

I agree that there's an overabundance of places to give constraints. We could force all associated type bounds/constraints to live on a where clause for the trait, but I think readability/convenience would suffer.

I find the shorthand for equality constraints a bit out of place - I assume you can't give actual type parameters for assoc types when using a trait, so it seems weird that you can specify constraints as if you could. I think that is a type is really internal then it should not be necessary to constrain it externally. I.e., this is a breach of encapsulation. Do you have an example of where it does make sense and/or is necessary?

I'm not sure I follow. Can you lay this out in a concrete example?

I do agree that the notation is a bit odd, but we need some solution for trait objects, as I'll argue below.

Which brings me to trait objects. I find this part of the proposal is getting really complex. Is there a simpler solution, like not allowing trait objects for traits with assoc types? I worry that up to this point, assoc types are very static and clearly resolvable, whereas for trait objects, we rely a lot on types which are not known at compile time. Firstly this is complicated, and secondly I worry about soundness edge cases here (although perhaps with minimal sub-traits stuff this won't be a problem). From an encapsulation perspective, it seems that anything outside the object should not have to care about the assoc types. From the perspective of trait objects, I would prefer that assoc items are treated like Self and 'erased' and thus can't be exposed out of the object. But I realise, this would severely restrict the usefulness of assoc types in general. Do you have examples of where trait objects with the exposed assoc types are useful/necessary?

I agree that there's some complexity here. However, I don't think we can simply not allow trait objects with associated types. Consider that Iterator and other container-related traits will move from generics to associated types -- and surely you should be able to use these with trait objects.

Is it correct that inherent assoc types are just scoped type aliases? Are these allowed today? And will this RFC change things here? If there is a change, do you have some motivation for inherant assoc types?

Yes, they are scoped type aliases, which are not allowed today. I will update the RFC with some motivation.

I prefer the tuple approach to multi-dispatch. Given that it is rare, I don't mind too much that it feels a little bit bolted on. Could you expand the first disadvantage please? I don't see what you mean there. It seems like if we went for the tuple approach we could then allow type parameters to be external, output params and be more backwards compatible and solve some of the complex edge cases mentioned above.

I'll update the RFC with elaboration on these points.

Member

aturon commented Aug 13, 2014

@nick29581

Looks good! My biggest question is what will happen with sub-traits - in particular, if a super trait declares associated items, are these inherited by the sub-trait? Can they be overridden? Can the bounds be specialised (as in virtual types)? I suspect we just want the simplest solution here, but I'm not sure exactly what that is.

This is an excellent point: I completely overlooked subtraits. I will work out a design and update the RFC.

My mental model for type params in traits is that there are two factors - 'input' vs 'output' wrt to impl selection and internal vs external in terms of the API. The node and edge types (as in the RFC) are internal whereas the type of items in a Vec is external. I think these two properties are mostly orthogonal. The two are conflated in the RFC. Could you give some examples of traits with 'external' type params and how they would work? E.g., Vec. I assume you would keep the type param as a type param, not as assoc type, but you don't want it to affect impl selection, is that an issue or not?

I don't understand the internal/external distinction. For the type Vec, you want a type parameter which can be instantiated in various ways. But for e.g. Container traits, the element type is always an output. For any given concrete Self type like Vec<u8> the element type is determined: it must be u8. And if you want to request a container with a given element type, you'd used Container<A=u8>. But I may be missing your point, here.

Do you have examples of where associated lifetimes are useful? I don't think I've seen these motivated before.

Basically any trait that today would take a lifetime argument would instead have an associated lifetime. Such traits are rare, but they do come up. I'll add examples/motivation to the RFC.

I'm a bit uncomfortable that we can constrain associated types using bounds, or where clauses on the trait, or where clauses on the assoc type. It seems like there ought to be one place to look not three. I guess if we move to only where clauses, that will get rid of bounds on assoc types too? I like requiring Self:: in the trait where clauses - that seems to indicate that the programmer should prefer local where clauses.

I agree that there's an overabundance of places to give constraints. We could force all associated type bounds/constraints to live on a where clause for the trait, but I think readability/convenience would suffer.

I find the shorthand for equality constraints a bit out of place - I assume you can't give actual type parameters for assoc types when using a trait, so it seems weird that you can specify constraints as if you could. I think that is a type is really internal then it should not be necessary to constrain it externally. I.e., this is a breach of encapsulation. Do you have an example of where it does make sense and/or is necessary?

I'm not sure I follow. Can you lay this out in a concrete example?

I do agree that the notation is a bit odd, but we need some solution for trait objects, as I'll argue below.

Which brings me to trait objects. I find this part of the proposal is getting really complex. Is there a simpler solution, like not allowing trait objects for traits with assoc types? I worry that up to this point, assoc types are very static and clearly resolvable, whereas for trait objects, we rely a lot on types which are not known at compile time. Firstly this is complicated, and secondly I worry about soundness edge cases here (although perhaps with minimal sub-traits stuff this won't be a problem). From an encapsulation perspective, it seems that anything outside the object should not have to care about the assoc types. From the perspective of trait objects, I would prefer that assoc items are treated like Self and 'erased' and thus can't be exposed out of the object. But I realise, this would severely restrict the usefulness of assoc types in general. Do you have examples of where trait objects with the exposed assoc types are useful/necessary?

I agree that there's some complexity here. However, I don't think we can simply not allow trait objects with associated types. Consider that Iterator and other container-related traits will move from generics to associated types -- and surely you should be able to use these with trait objects.

Is it correct that inherent assoc types are just scoped type aliases? Are these allowed today? And will this RFC change things here? If there is a change, do you have some motivation for inherant assoc types?

Yes, they are scoped type aliases, which are not allowed today. I will update the RFC with some motivation.

I prefer the tuple approach to multi-dispatch. Given that it is rare, I don't mind too much that it feels a little bit bolted on. Could you expand the first disadvantage please? I don't see what you mean there. It seems like if we went for the tuple approach we could then allow type parameters to be external, output params and be more backwards compatible and solve some of the complex edge cases mentioned above.

I'll update the RFC with elaboration on these points.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 13, 2014

Member

@glaebhoerl

It should be noted that GHC doesn't allow attaching constraints (where clauses) directly to type aliases, associated or otherwise. (I'm not familiar with the specific reasons, but suspect there might be good ones.)

It turns out that you can constrain associates type synonyms in Haskell, provided that you turn on FlexibleInstances: http://www.haskell.org/pipermail/haskell-cafe/2011-March/090449.html

Member

aturon commented Aug 13, 2014

@glaebhoerl

It should be noted that GHC doesn't allow attaching constraints (where clauses) directly to type aliases, associated or otherwise. (I'm not familiar with the specific reasons, but suspect there might be good ones.)

It turns out that you can constrain associates type synonyms in Haskell, provided that you turn on FlexibleInstances: http://www.haskell.org/pipermail/haskell-cafe/2011-March/090449.html

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Aug 13, 2014

Contributor

So I read over the RFC. After a preliminary read, here are some comments. I'm going to think more particularly about unification of types and == bounds.

1. As we discussed earlier, I think that we should restrict object types to have exactly one instance of any particular trait, even if there are multiple input types, so as to avoid painful inference quandries. We can always lift those restrictions later.

2. Within trait bodies, I think it is strange that where clauses are tied to associated type declarations. After all, they have no particular connection to that type parameter -- in every other case, the where clause is attached to the declaration as a whole. Perhaps where clauses should just be freestanding within the trait body? For example:

trait Foo {
    type X; type Y;
    where X : Eq;
}

3. Is there any conceptual difference between a where clause attached to the trait header and one attached to the body? Perhaps there is a slight difference with respect to well-formedness criteria, but I am not sure. That is, I would assume that if I write

trait Add<RHS> where RHS : Eq { ... }

then it is a violation to even write a trait bound Add<Baz> where Baz does not implement Eq. But where clauses within the trait body would not have this connotation? On the other hand, I would expect that if I write Add<SomeOutput=Foo> and the trait Add declares that SomeOutput : Eq then Foo must implement Eq.

4. You probably want to include the possibility of lifetimes having bounds, per https://github.com/rust-lang/rfcs/blob/master/active/0049-bounds-on-object-and-generic-types.md

5. You say that a reference Self::ID is "rewritten' as <Self as Trait>::ID -- but I would except that in fact Self is treated exactly like any other type parameter, which means that we will search through the bounds declared on Self (supertraits etc) and try to figure out the trait from which ID derives. It's worth spelling out just how type parameters are treated, since it's actually somewhat different than other types.

Actually, in writing this, I realized I wasn't quite sure what it ought to be. At first I thought that we first search the bounds to try and elaborate the precise trait reference, and then look for applicable impls only if nothing is found (which would have to be some sort of blanket impl). But it occurs to me that searching for impls can yield more precise information than what is contained in the bound, since the blanket impl would specify precise values for associated types, and the bound may not. So perhaps we should search for an impl first and only fallback to bounds if nothing is found? The problem there is that, in a multidispatch scenario, the bound may be needed to inform us as to what was meant. And in that case, the impl may then further help provide specifics. So maybe you can't just seach one.

Here is an example of what the heck I am talking about:

trait Add<RHS> { type SUM; fn add(self, rhs: RHS) -> SUM; }

fn foo<T:Add<()>>() {
    let x: T::SUM = ...;
}

impl<T> Add<()> for T {
    type SUM = T;
    fn add(self, rhs: ()) -> T { self }
}

Here the reference T::SUM inside of foo() is not really supplying the full details, since it is not specifying what trait SUM derives from nor the full set of input types. For convenience, we said we would want to use the bounds on T to elaborate T::SUM to <T as Add<()>>::SUM. But in that case, we can identify a blanket impl that applies, and map SUM to ().

So as I said, we should try to carefully write out the algorithm, probably using "trait-match" as a helpful subroutine. Let's take some time to do this later.

6. In general, I think we should sit down and carefully spell out the search procedure a bit more. This will require some collaboration with "trait reform", I think (which, as we've discussed, likely needs some amending).

Contributor

nikomatsakis commented Aug 13, 2014

So I read over the RFC. After a preliminary read, here are some comments. I'm going to think more particularly about unification of types and == bounds.

1. As we discussed earlier, I think that we should restrict object types to have exactly one instance of any particular trait, even if there are multiple input types, so as to avoid painful inference quandries. We can always lift those restrictions later.

2. Within trait bodies, I think it is strange that where clauses are tied to associated type declarations. After all, they have no particular connection to that type parameter -- in every other case, the where clause is attached to the declaration as a whole. Perhaps where clauses should just be freestanding within the trait body? For example:

trait Foo {
    type X; type Y;
    where X : Eq;
}

3. Is there any conceptual difference between a where clause attached to the trait header and one attached to the body? Perhaps there is a slight difference with respect to well-formedness criteria, but I am not sure. That is, I would assume that if I write

trait Add<RHS> where RHS : Eq { ... }

then it is a violation to even write a trait bound Add<Baz> where Baz does not implement Eq. But where clauses within the trait body would not have this connotation? On the other hand, I would expect that if I write Add<SomeOutput=Foo> and the trait Add declares that SomeOutput : Eq then Foo must implement Eq.

4. You probably want to include the possibility of lifetimes having bounds, per https://github.com/rust-lang/rfcs/blob/master/active/0049-bounds-on-object-and-generic-types.md

5. You say that a reference Self::ID is "rewritten' as <Self as Trait>::ID -- but I would except that in fact Self is treated exactly like any other type parameter, which means that we will search through the bounds declared on Self (supertraits etc) and try to figure out the trait from which ID derives. It's worth spelling out just how type parameters are treated, since it's actually somewhat different than other types.

Actually, in writing this, I realized I wasn't quite sure what it ought to be. At first I thought that we first search the bounds to try and elaborate the precise trait reference, and then look for applicable impls only if nothing is found (which would have to be some sort of blanket impl). But it occurs to me that searching for impls can yield more precise information than what is contained in the bound, since the blanket impl would specify precise values for associated types, and the bound may not. So perhaps we should search for an impl first and only fallback to bounds if nothing is found? The problem there is that, in a multidispatch scenario, the bound may be needed to inform us as to what was meant. And in that case, the impl may then further help provide specifics. So maybe you can't just seach one.

Here is an example of what the heck I am talking about:

trait Add<RHS> { type SUM; fn add(self, rhs: RHS) -> SUM; }

fn foo<T:Add<()>>() {
    let x: T::SUM = ...;
}

impl<T> Add<()> for T {
    type SUM = T;
    fn add(self, rhs: ()) -> T { self }
}

Here the reference T::SUM inside of foo() is not really supplying the full details, since it is not specifying what trait SUM derives from nor the full set of input types. For convenience, we said we would want to use the bounds on T to elaborate T::SUM to <T as Add<()>>::SUM. But in that case, we can identify a blanket impl that applies, and map SUM to ().

So as I said, we should try to carefully write out the algorithm, probably using "trait-match" as a helpful subroutine. Let's take some time to do this later.

6. In general, I think we should sit down and carefully spell out the search procedure a bit more. This will require some collaboration with "trait reform", I think (which, as we've discussed, likely needs some amending).

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Aug 13, 2014

Contributor

It turns out that you can constrain associates type synonyms in Haskell, provided that you turn on FlexibleInstances: http://www.haskell.org/pipermail/haskell-cafe/2011-March/090449.html

I wasn't very clear, sorry. You can put constraints on associated types in the class "head" (as in the linked example). If in a Rust trait, a where clause directly attached to an associated type is just a different syntax for writing the same thing (which it is), then there's no problem with that. (Although only allowing type Foo: Trait but not type Foo where ..., and only allowing where clauses on the trait "head", might be a decent way to cut down on the plethora of equivalent syntactic formulations, should we want to.)

What GHC doesn't allow, but is present as an example in the RFC, is constraints on top-level type synonyms:

type Apply<Name, Elt> where Name: TypeToType<Elt> = Name::Output;

I remember that people have asked about why this isn't allowed, and that sensible reasons were given, but not exactly what they were.

Contributor

glaebhoerl commented Aug 13, 2014

It turns out that you can constrain associates type synonyms in Haskell, provided that you turn on FlexibleInstances: http://www.haskell.org/pipermail/haskell-cafe/2011-March/090449.html

I wasn't very clear, sorry. You can put constraints on associated types in the class "head" (as in the linked example). If in a Rust trait, a where clause directly attached to an associated type is just a different syntax for writing the same thing (which it is), then there's no problem with that. (Although only allowing type Foo: Trait but not type Foo where ..., and only allowing where clauses on the trait "head", might be a decent way to cut down on the plethora of equivalent syntactic formulations, should we want to.)

What GHC doesn't allow, but is present as an example in the RFC, is constraints on top-level type synonyms:

type Apply<Name, Elt> where Name: TypeToType<Elt> = Name::Output;

I remember that people have asked about why this isn't allowed, and that sensible reasons were given, but not exactly what they were.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 13, 2014

Member

@glaebhoerl

The RFC only talks about associated typedefs/aliases/synonyms, but GHC also has associated datatypes (and in fact had them first).

You're right: there are a few "items" that cannot be associated with a trait under this proposal. I think this RFC lays the groundwork for adding other associated items, backwards-compatibly, in the future. (And it covers the most essential cases for library stabilization.)

It's possible to use an auxiliary trait to "recover" input type syntax for an associated output type ...

That's a good point. I'll add it to the Alternatives section, though I agree with you that providing sugar is preferable.

All trait items (including methods, understood as UFCS functions) are in scope for the trait body.

This option seems overly aggressive: there is little benefit to writing some_method(self, arg) rather than self.some_method(arg), and the UFCS version loses autoderef etc.

Perhaps there's little benefit, but is there a known drawback?

To me, it seems strange to automatically have methods in scope as UFCS-style functions when writing trait bodies, but not elsewhere. Generally, we tend toward putting as few things into scope as possible, modulo ergonomics -- but here the ergonomics favor calling methods via method notation anyway.

Member

aturon commented Aug 13, 2014

@glaebhoerl

The RFC only talks about associated typedefs/aliases/synonyms, but GHC also has associated datatypes (and in fact had them first).

You're right: there are a few "items" that cannot be associated with a trait under this proposal. I think this RFC lays the groundwork for adding other associated items, backwards-compatibly, in the future. (And it covers the most essential cases for library stabilization.)

It's possible to use an auxiliary trait to "recover" input type syntax for an associated output type ...

That's a good point. I'll add it to the Alternatives section, though I agree with you that providing sugar is preferable.

All trait items (including methods, understood as UFCS functions) are in scope for the trait body.

This option seems overly aggressive: there is little benefit to writing some_method(self, arg) rather than self.some_method(arg), and the UFCS version loses autoderef etc.

Perhaps there's little benefit, but is there a known drawback?

To me, it seems strange to automatically have methods in scope as UFCS-style functions when writing trait bodies, but not elsewhere. Generally, we tend toward putting as few things into scope as possible, modulo ergonomics -- but here the ergonomics favor calling methods via method notation anyway.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 13, 2014

Member

@glaebhoerl

What GHC doesn't allow, but is present as an example in the RFC, is constraints on top-level type synonyms ...

I see. The intent was that where clauses on associated types would function the same as where clauses on the trait itself (modulo scoping differences). But it's becoming clear through these comments that this is redundant and confusing. So I plan to revise the RFC to drop where clauses on associated types (but keep bounds).

Member

aturon commented Aug 13, 2014

@glaebhoerl

What GHC doesn't allow, but is present as an example in the RFC, is constraints on top-level type synonyms ...

I see. The intent was that where clauses on associated types would function the same as where clauses on the trait itself (modulo scoping differences). But it's becoming clear through these comments that this is redundant and confusing. So I plan to revise the RFC to drop where clauses on associated types (but keep bounds).

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 13, 2014

Member

@ben0x539

re: grammar for where clauses, the new plan is to drop where clauses from associated types altogether.

Member

aturon commented Aug 13, 2014

@ben0x539

re: grammar for where clauses, the new plan is to drop where clauses from associated types altogether.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 13, 2014

Member

@nikomatsakis

Within trait bodies, I think it is strange that where clauses are tied to associated type declarations. After all, they have no particular connection to that type parameter -- in every other case, the where clause is attached to the declaration as a whole.

Our intent is to allow where clauses on trait methods, so that's not quite right, but I take your broader point, and I think we should drop the where clauses on associated types.

Personally, I think the where for trait definitions should still go outside the body, though, to fit with the general where clause syntax. We might want to reconsider whether associated types are in scope for the clause, though.

For your other points, I need to think/chat a bit more.

Member

aturon commented Aug 13, 2014

@nikomatsakis

Within trait bodies, I think it is strange that where clauses are tied to associated type declarations. After all, they have no particular connection to that type parameter -- in every other case, the where clause is attached to the declaration as a whole.

Our intent is to allow where clauses on trait methods, so that's not quite right, but I take your broader point, and I think we should drop the where clauses on associated types.

Personally, I think the where for trait definitions should still go outside the body, though, to fit with the general where clause syntax. We might want to reconsider whether associated types are in scope for the clause, though.

For your other points, I need to think/chat a bit more.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 13, 2014

Member

@ben0x539

Would where-clauses on associated types allow a trait like

trait Add {
    type LHS;
    type RHS where (LHS, RHS) = Self;
    type SUM;
    fn add(&LHS, &RHS) -> SUM;
}

that can be implemented mostly like the "Multidispatch through tuple types" alternative example?

That's a very interesting idea, which would give you a nice way to use multidispatch with associated functions!

The current where clause syntax doesn't allow these kinds of equality constraints, but they're essentially forced on us for associated types. I need to amend the RFC to discuss them explicitly -- allowing arbitrary equality constraints may have complications for the typechecker.

Member

aturon commented Aug 13, 2014

@ben0x539

Would where-clauses on associated types allow a trait like

trait Add {
    type LHS;
    type RHS where (LHS, RHS) = Self;
    type SUM;
    fn add(&LHS, &RHS) -> SUM;
}

that can be implemented mostly like the "Multidispatch through tuple types" alternative example?

That's a very interesting idea, which would give you a nice way to use multidispatch with associated functions!

The current where clause syntax doesn't allow these kinds of equality constraints, but they're essentially forced on us for associated types. I need to amend the RFC to discuss them explicitly -- allowing arbitrary equality constraints may have complications for the typechecker.

@rkjnsn

This comment has been minimized.

Show comment
Hide comment
@rkjnsn

rkjnsn Aug 13, 2014

Contributor

+1
I really like this proposal.

Contributor

rkjnsn commented Aug 13, 2014

+1
I really like this proposal.

@Ericson2314

This comment has been minimized.

Show comment
Hide comment
@Ericson2314

Ericson2314 Aug 14, 2014

Contributor

Sorry for the self plug, but I have a in progress RFC which coincidentally builds on this https://github.com/Ericson2314/rfcs/blob/master/active/0000-combine-mod-trait.md

Contributor

Ericson2314 commented Aug 14, 2014

Sorry for the self plug, but I have a in progress RFC which coincidentally builds on this https://github.com/Ericson2314/rfcs/blob/master/active/0000-combine-mod-trait.md

@nrc

This comment has been minimized.

Show comment
Hide comment
@nrc

nrc Aug 15, 2014

Member

@aturon internal/external is a hand wavey concept, but I think a useful if vague distinction. I realise they are not orthogonal, only output parameters have this distinction, all input parameters are external. My hypothesis is that type params are either external in that they are always specified by the user of the type and it does not make sense for the definition to override them or internal where they are specified by implementations and it does not make sense for the user to specify them. For example, T in Container<T> is external - it would be silly to implement Container<Foo> rather than Container<T>. Whereas, for Graph it makes sense for the implementor to specify the Node and Edge types and would be a bit weird for a User to specify them. Furthermore, external params are ones where the user should refer to them by the concrete type - a user of Container<int> wants to get ints out. Internal params should be referred to only by the abstract type, i.e., you should use MyGraph::Node rather than MyNode.

To be succinct, I think there are type params which are best expressed as type params, even on traits, rather than associated types.

Although this kind of gives two ways to do 'the same' thing, it reduces complexity elsewhere, since you don't need to add the type binding in param position syntax or the complexity around trait objects - only the external params need to be specified and we could (maybe) ban trait objects with assoc types because things that need binding should be external. (I need to think about Iterator - perhaps it is OK, because the container you are iterating over should never be exposed outside the Iterator, so you don't need to know it from outside the trait object).

Member

nrc commented Aug 15, 2014

@aturon internal/external is a hand wavey concept, but I think a useful if vague distinction. I realise they are not orthogonal, only output parameters have this distinction, all input parameters are external. My hypothesis is that type params are either external in that they are always specified by the user of the type and it does not make sense for the definition to override them or internal where they are specified by implementations and it does not make sense for the user to specify them. For example, T in Container<T> is external - it would be silly to implement Container<Foo> rather than Container<T>. Whereas, for Graph it makes sense for the implementor to specify the Node and Edge types and would be a bit weird for a User to specify them. Furthermore, external params are ones where the user should refer to them by the concrete type - a user of Container<int> wants to get ints out. Internal params should be referred to only by the abstract type, i.e., you should use MyGraph::Node rather than MyNode.

To be succinct, I think there are type params which are best expressed as type params, even on traits, rather than associated types.

Although this kind of gives two ways to do 'the same' thing, it reduces complexity elsewhere, since you don't need to add the type binding in param position syntax or the complexity around trait objects - only the external params need to be specified and we could (maybe) ban trait objects with assoc types because things that need binding should be external. (I need to think about Iterator - perhaps it is OK, because the container you are iterating over should never be exposed outside the Iterator, so you don't need to know it from outside the trait object).

active/0000-associated-items.md
+}
+
+impl Add<int> for int {
+ type SUM = int;

This comment has been minimized.

@bstrie

bstrie Aug 20, 2014

Contributor

Would this line be necessary, given that you're using int directly as a return type on the next line?

@bstrie

bstrie Aug 20, 2014

Contributor

Would this line be necessary, given that you're using int directly as a return type on the next line?

This comment has been minimized.

@aturon

aturon Aug 20, 2014

Member

In principle, you could probably infer the associated type given the signatures elsewhere, but that's not part of this RFC (and we could always add it later).

@aturon

aturon Aug 20, 2014

Member

In principle, you could probably infer the associated type given the signatures elsewhere, but that's not part of this RFC (and we could always add it later).

This comment has been minimized.

@apoelstra

apoelstra Aug 21, 2014

Contributor

It's hard to say without usage, but I suspect that forcing type SUM = int to be explicit will gain more in clarity than it'll lose in extra typing.

@apoelstra

apoelstra Aug 21, 2014

Contributor

It's hard to say without usage, but I suspect that forcing type SUM = int to be explicit will gain more in clarity than it'll lose in extra typing.

active/0000-associated-items.md
+## Scoping of associated items
+
+Associated types are frequently referred to in the signatures of a trait's
+methods and associated functions, and it is natural and convneient to refer to

This comment has been minimized.

@cmr

cmr Aug 21, 2014

Member

typo: convenient

@cmr

cmr Aug 21, 2014

Member

typo: convenient

@cmr

This comment has been minimized.

Show comment
Hide comment
@cmr

cmr Aug 21, 2014

Member

How would it handle name resolution for:

trait ShaderProgram { ... }
trait Device {
    type ShaderProgram : ShaderProgram;
    fn create_shader_program(&mut self) -> ShaderProgram;
    // or even trickier:
    fn copy_shader_program<S: ShaderProgram>(&mut self, S) -> ShaderProgram;
}

In particular, is the return type of those methods a DST, referring to the trait (and probably an error)? Or is it referring to the associated type?

Somewhat related, is there any usecase for "associated traits", which also might cause shadowing pain were we to add them?

(This isn't contrived; it's cut down from me trying to model the Metal API with "static" types (as opposed to existentials, which they use heavily) under this proposal)

Member

cmr commented Aug 21, 2014

How would it handle name resolution for:

trait ShaderProgram { ... }
trait Device {
    type ShaderProgram : ShaderProgram;
    fn create_shader_program(&mut self) -> ShaderProgram;
    // or even trickier:
    fn copy_shader_program<S: ShaderProgram>(&mut self, S) -> ShaderProgram;
}

In particular, is the return type of those methods a DST, referring to the trait (and probably an error)? Or is it referring to the associated type?

Somewhat related, is there any usecase for "associated traits", which also might cause shadowing pain were we to add them?

(This isn't contrived; it's cut down from me trying to model the Metal API with "static" types (as opposed to existentials, which they use heavily) under this proposal)

@cmr

This comment has been minimized.

Show comment
Hide comment
@cmr

cmr Aug 21, 2014

Member

Complete example, which would be entirely unrealistic without associated types: https://github.com/cmr/rust-metal-sketch/blob/master/src/lib.rs#L14

Member

cmr commented Aug 21, 2014

Complete example, which would be entirely unrealistic without associated types: https://github.com/cmr/rust-metal-sketch/blob/master/src/lib.rs#L14

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Aug 22, 2014

Contributor

Somewhat related, is there any usecase for "associated traits", which also might cause shadowing pain were we to add them?

Yes - overlapping with the use case for being able to parameterize generics over traits (e.g. fn foo<trait Foo, T: Foo>()), called ConstraintKinds in GHC, with which type aliases and associated types can also have kind trait (e.g. type IterInt = Iterator<int>; fn foo<Iter: IterInt>(...)).

Maybe it's only useful together with HKT, at least the use cases I can think of right now involve them - in any case, going with the associated traits formulation:

trait Lookup {
    trait KeyConstraint;
    fn lookup<Key: KeyConstraint, Val>(self: &Self<Key, Val>, key: &Key) -> &Val;
}

impl Lookup for HashMap {
    trait KeyConstraint: Hash + Eq { }
    /* implement lookup assuming Key: Hash + Eq */
}

impl<T: Hash + Eq> HashMap::KeyConstraint for T { }

impl Lookup for TreeMap {
    trait KeyConstraint: Ord { }
    /* implement lookup assuming Key: Ord */
}

impl<T: Ord> TreeMap::KeyConstraint for T { }

(I'm assuming a simplified HashMap which doesn't have a separate hasher parameter just for the sake of example. The type-aliases-for-traits formulation would avoid the need for the boilerplate Foo::KeyConstraint impls above. If the code isn't clear, feel free to ask.)

Contributor

glaebhoerl commented Aug 22, 2014

Somewhat related, is there any usecase for "associated traits", which also might cause shadowing pain were we to add them?

Yes - overlapping with the use case for being able to parameterize generics over traits (e.g. fn foo<trait Foo, T: Foo>()), called ConstraintKinds in GHC, with which type aliases and associated types can also have kind trait (e.g. type IterInt = Iterator<int>; fn foo<Iter: IterInt>(...)).

Maybe it's only useful together with HKT, at least the use cases I can think of right now involve them - in any case, going with the associated traits formulation:

trait Lookup {
    trait KeyConstraint;
    fn lookup<Key: KeyConstraint, Val>(self: &Self<Key, Val>, key: &Key) -> &Val;
}

impl Lookup for HashMap {
    trait KeyConstraint: Hash + Eq { }
    /* implement lookup assuming Key: Hash + Eq */
}

impl<T: Hash + Eq> HashMap::KeyConstraint for T { }

impl Lookup for TreeMap {
    trait KeyConstraint: Ord { }
    /* implement lookup assuming Key: Ord */
}

impl<T: Ord> TreeMap::KeyConstraint for T { }

(I'm assuming a simplified HashMap which doesn't have a separate hasher parameter just for the sake of example. The type-aliases-for-traits formulation would avoid the need for the boilerplate Foo::KeyConstraint impls above. If the code isn't clear, feel free to ask.)

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 22, 2014

Member

@cmr With the new rules prohibiting shadowing, I think your example would probably just be an error.

Thanks for the pointer to your trait example; that's a pretty compelling argument, I think :-)

Member

aturon commented Aug 22, 2014

@cmr With the new rules prohibiting shadowing, I think your example would probably just be an error.

Thanks for the pointer to your trait example; that's a pretty compelling argument, I think :-)

@cmr

This comment has been minimized.

Show comment
Hide comment
@cmr

cmr Aug 22, 2014

Member

The restriction on trait objects containing associated types makes them useless for my usecase: dynamically switching rendering APIs at runtime. Say:

trait Device {
    type CommandBuffer;
    fn create_command_buffer(&mut self) -> CommandBuffer;

Every Device implementation would provide a unique CommandBuffer, and thus requiring that the precise CommandBuffer type be specified determines which Device implementation you can use in practice.

This further enforces the "design boundary" between using generics and using trait objects. If I want my trait to be usable as a trait object, I need to box everything, leading to allocations, which is very unidiomatic and is forced even when the runtime dispatch is not desired. This isn't a problem specifically with this proposal, but it only exacerbates the problem.

Member

cmr commented Aug 22, 2014

The restriction on trait objects containing associated types makes them useless for my usecase: dynamically switching rendering APIs at runtime. Say:

trait Device {
    type CommandBuffer;
    fn create_command_buffer(&mut self) -> CommandBuffer;

Every Device implementation would provide a unique CommandBuffer, and thus requiring that the precise CommandBuffer type be specified determines which Device implementation you can use in practice.

This further enforces the "design boundary" between using generics and using trait objects. If I want my trait to be usable as a trait object, I need to box everything, leading to allocations, which is very unidiomatic and is forced even when the runtime dispatch is not desired. This isn't a problem specifically with this proposal, but it only exacerbates the problem.

@cmr cmr referenced this pull request in gfx-rs/gfx Aug 23, 2014

Closed

Add support for switching render backend at runtime #56

@Ericson2314

This comment has been minimized.

Show comment
Hide comment
@Ericson2314

Ericson2314 Aug 25, 2014

Contributor

@cmr, @aturon: if a method returns a bounded associated type, could the method on the trait object return a trait object of that bound? Would that help?

@glaebhoerl: Good point. This would also allow us to define a proper treeset or hashset monads, etc.

Contributor

Ericson2314 commented Aug 25, 2014

@cmr, @aturon: if a method returns a bounded associated type, could the method on the trait object return a trait object of that bound? Would that help?

@glaebhoerl: Good point. This would also allow us to define a proper treeset or hashset monads, etc.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 26, 2014

Member

@cmr It seems like dynamic dispatch is unavoidable in that usecase: if you have a Box<Device>, you wouldn't statically know the associated CommandBuffer type, so method invocation on it would have to use a vtable. And if you're allowed to hand out that CommandBuffer as a value, the vtable has to come with it, so allocation seems forced as well.

But maybe there's an implementation strategy for trait objects I'm missing that would handle the case you have in mind?

Finally, the proposal makes it an error to leave off associated type constraints for trait objects -- and that means that in the future, if we see some way to make sense of "erased" associated types in trait objects, we could add support later on.

Member

aturon commented Aug 26, 2014

@cmr It seems like dynamic dispatch is unavoidable in that usecase: if you have a Box<Device>, you wouldn't statically know the associated CommandBuffer type, so method invocation on it would have to use a vtable. And if you're allowed to hand out that CommandBuffer as a value, the vtable has to come with it, so allocation seems forced as well.

But maybe there's an implementation strategy for trait objects I'm missing that would handle the case you have in mind?

Finally, the proposal makes it an error to leave off associated type constraints for trait objects -- and that means that in the future, if we see some way to make sense of "erased" associated types in trait objects, we could add support later on.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 26, 2014

Member

@cmr Could you spell out a bit more of the API you have in mind? It'd be helpful to see how CommandBuffer is ultimately used.

Member

aturon commented Aug 26, 2014

@cmr Could you spell out a bit more of the API you have in mind? It'd be helpful to see how CommandBuffer is ultimately used.

@cmr

This comment has been minimized.

Show comment
Hide comment
@cmr

cmr Aug 26, 2014

Member

@aturon Yes, I agree the allocation is unavoidable, but there's no way to avoid the allocation in the non-trait object case (which is the most common). A more complete example:

struct Mesh;
struct BlendMode;

trait Device {
    type ConcreteCommandBuffer : CommandBuffer;
    fn create_command_buffer(&mut self) -> ConcreteCommandBuffer;
    fn submit(&mut self, cb: ConcreteCommandBuffer);
}

trait CommandBuffer {
    fn draw(&mut self, mesh: &Mesh);
    fn blend_mode(&mut self, mode: &BlendMode);
}

fn main() {
    let mut device: GlDevice = GlDevice::new();
    let mut cb = device.create_command_buffer();
    cb.draw(&make_mesh());
    device.submit(cb);
}

This is the "normal" static case, but for a runtime-switchable thing you'd want:

fn main() {
    let mut device: Box<Device>;
    if cli_arg_use_directx() {
        device = box D3DDevice::new();
    } else {
        device = box GlDevice::new();
    }

    let mut cb: Box<CommandBuffer> = device.create_command_buffer();
    // ...
}

Introducing lots of "unnecessary" allocations in the static case where there could be none is what I refer to as the "design boundary". Either you design for trait objects, or you can't really use them.

Member

cmr commented Aug 26, 2014

@aturon Yes, I agree the allocation is unavoidable, but there's no way to avoid the allocation in the non-trait object case (which is the most common). A more complete example:

struct Mesh;
struct BlendMode;

trait Device {
    type ConcreteCommandBuffer : CommandBuffer;
    fn create_command_buffer(&mut self) -> ConcreteCommandBuffer;
    fn submit(&mut self, cb: ConcreteCommandBuffer);
}

trait CommandBuffer {
    fn draw(&mut self, mesh: &Mesh);
    fn blend_mode(&mut self, mode: &BlendMode);
}

fn main() {
    let mut device: GlDevice = GlDevice::new();
    let mut cb = device.create_command_buffer();
    cb.draw(&make_mesh());
    device.submit(cb);
}

This is the "normal" static case, but for a runtime-switchable thing you'd want:

fn main() {
    let mut device: Box<Device>;
    if cli_arg_use_directx() {
        device = box D3DDevice::new();
    } else {
        device = box GlDevice::new();
    }

    let mut cb: Box<CommandBuffer> = device.create_command_buffer();
    // ...
}

Introducing lots of "unnecessary" allocations in the static case where there could be none is what I refer to as the "design boundary". Either you design for trait objects, or you can't really use them.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Aug 26, 2014

Member

@cmr I see, thanks for clearing that up.

I completely agree with your analysis: the current trait system has "cliffs" when you want to transition to trait objects.

There are various things we could do in the future to provide "auto-boxing" in this kind of situation; I know @nikomatsakis has thought about it, at least. That kind of thing should be backwards-compatible.

In any case, as you said, this isn't really particular to associated items, but having associated types makes the problem all the more visible.

Member

aturon commented Aug 26, 2014

@cmr I see, thanks for clearing that up.

I completely agree with your analysis: the current trait system has "cliffs" when you want to transition to trait objects.

There are various things we could do in the future to provide "auto-boxing" in this kind of situation; I know @nikomatsakis has thought about it, at least. That kind of thing should be backwards-compatible.

In any case, as you said, this isn't really particular to associated items, but having associated types makes the problem all the more visible.

@Kimundi

This comment has been minimized.

Show comment
Hide comment
@Kimundi

Kimundi Aug 27, 2014

Member

Its often possible to make a trait "dynamic-proof" by adding additional methods like this though:

trait Device {
    type ConcreteCommandBuffer : CommandBuffer;
    fn create_command_buffer(&mut self) -> ConcreteCommandBuffer;
    fn submit(&mut self, cb: ConcreteCommandBuffer);

    fn create_command_buffer_dynamic(&mut self) -> Box<CommandBuffer> {
        box self.create_command_buffer()
    }
    fn submit_dynamic(&mut self, cb: &CommandBuffer) {
        assert!(cb.is::<ConcreteCommandBuffer>()) // Needs some kind of `Any` hacked into it
        self.submit(c.downcast_ref::<ConcreteCommandBuffer>())
    }
}

But yeah, the ability to have autoboxing in such situations would be useful.

Member

Kimundi commented Aug 27, 2014

Its often possible to make a trait "dynamic-proof" by adding additional methods like this though:

trait Device {
    type ConcreteCommandBuffer : CommandBuffer;
    fn create_command_buffer(&mut self) -> ConcreteCommandBuffer;
    fn submit(&mut self, cb: ConcreteCommandBuffer);

    fn create_command_buffer_dynamic(&mut self) -> Box<CommandBuffer> {
        box self.create_command_buffer()
    }
    fn submit_dynamic(&mut self, cb: &CommandBuffer) {
        assert!(cb.is::<ConcreteCommandBuffer>()) // Needs some kind of `Any` hacked into it
        self.submit(c.downcast_ref::<ConcreteCommandBuffer>())
    }
}

But yeah, the ability to have autoboxing in such situations would be useful.

@Ericson2314

This comment has been minimized.

Show comment
Hide comment
@Ericson2314

Ericson2314 Aug 28, 2014

Contributor

It's a bit awkward, but it does get the desired run-time representation:

fn main() {
    let mut device: Box<Device>;
    if cli_arg_use_directx() {
        rest(D3DDevice::new());
    } else {
        rest(GlDevice::new());
    }
}

fn rest<D>(device: D) where D: Device {
    let mut cb: D::CommandBuffer = device.create_command_buffer();
    cb.draw(&make_mesh());
    device.submit(cb);
}

The problem is code that isn't generic across implementations must be either consolidate in one trait, or one ends up write a gazillion traits.

Contributor

Ericson2314 commented Aug 28, 2014

It's a bit awkward, but it does get the desired run-time representation:

fn main() {
    let mut device: Box<Device>;
    if cli_arg_use_directx() {
        rest(D3DDevice::new());
    } else {
        rest(GlDevice::new());
    }
}

fn rest<D>(device: D) where D: Device {
    let mut cb: D::CommandBuffer = device.create_command_buffer();
    cb.draw(&make_mesh());
    device.submit(cb);
}

The problem is code that isn't generic across implementations must be either consolidate in one trait, or one ends up write a gazillion traits.

@jganetsk

This comment has been minimized.

Show comment
Hide comment
@jganetsk

jganetsk Aug 31, 2014

Sounds to me like Rust people are finally rediscovering the Standard ML module system.

Sounds to me like Rust people are finally rediscovering the Standard ML module system.

@arielb1

This comment has been minimized.

Show comment
Hide comment
@arielb1

arielb1 Sep 1, 2014

Contributor

@cmr

Having the CommandBuffer associated with the dynamic Box<Device> object essentially requires dependent types (because you can create multiple Box<Device>-s, and you need to ensure only the right CommandBuffer is used with the right Box<Device>), which are a big "non-feature".

Contributor

arielb1 commented Sep 1, 2014

@cmr

Having the CommandBuffer associated with the dynamic Box<Device> object essentially requires dependent types (because you can create multiple Box<Device>-s, and you need to ensure only the right CommandBuffer is used with the right Box<Device>), which are a big "non-feature".

@cmr

This comment has been minimized.

Show comment
Hide comment
@cmr

cmr Sep 1, 2014

Member

@arielb1 Not really a problem, would query for type equality with Any. It's already opting into dynamic dispatch.

Member

cmr commented Sep 1, 2014

@arielb1 Not really a problem, would query for type equality with Any. It's already opting into dynamic dispatch.

brendanzab added a commit to brendanzab/gfx-rs that referenced this pull request Sep 1, 2014

Move gl_device into separate directory
This will be converted into a crate once associated items are added and used in the `Device` trait. See rust-lang/rfcs#195

brendanzab added a commit to brendanzab/gfx-rs that referenced this pull request Sep 1, 2014

Move gl_device into separate directory
This will be converted into a crate once associated items are added and used in the `Device` trait. See rust-lang/rfcs#195
@arielb1

This comment has been minimized.

Show comment
Hide comment
@arielb1

arielb1 Sep 2, 2014

Contributor

@cmr

You essentially want to have associated type objects? Something like Box<Device::CommandBuffer>, and to be able to combine it with a Box<Device> and fail if the types are wrong?

Contributor

arielb1 commented Sep 2, 2014

@cmr

You essentially want to have associated type objects? Something like Box<Device::CommandBuffer>, and to be able to combine it with a Box<Device> and fail if the types are wrong?

@arielb1

This comment has been minimized.

Show comment
Hide comment
@arielb1

arielb1 Sep 3, 2014

Contributor

The HKT encoding does not seem to actually work.

Suppose you have a mutable version of the Iterator example:

trait IterableOwned {
    type A;
    type I: Iterator<A>;
    fn iter_owned(self) -> I;
}

trait MutIterable {
    fn mut_iter<'a>(&'a mut self) -> <&'a mut Self>::I where &'a mut Self: IterableOwned {
        IterableOwned::iter_owned(self)
    }
}

Suppose you want to write a function that takes an iterable of numbers and subtracts
the average. That function should be something like:

fn normalise<T: MutIterable<f64>+Collection>(it: &'a mut T) {
  let mean = it.mut_iter().sum() / it.len(); // This would be the same if
                                             //   I used iter instead of
                                             //   mut_iter

  for i in it.mut_iter() {
    *i -= mean;
  }
}

However, this wouldn't work - the compiler wouldn't be able to figure out that &'a mut T implements IterableOwned as I didn't pass a bound. You may want to add a bound in a where clause, something like where &'a mut T: IterableOwned, but the lifetime 'a must be shorter than the lifetime of normalise - as otherwise we would have conflicting borrows of it! - we really want a ∏₁ (universal) lifetime bound - something like ∀'a &'a mut T - but there is no plan for supporting these kind of bounds.

Contributor

arielb1 commented Sep 3, 2014

The HKT encoding does not seem to actually work.

Suppose you have a mutable version of the Iterator example:

trait IterableOwned {
    type A;
    type I: Iterator<A>;
    fn iter_owned(self) -> I;
}

trait MutIterable {
    fn mut_iter<'a>(&'a mut self) -> <&'a mut Self>::I where &'a mut Self: IterableOwned {
        IterableOwned::iter_owned(self)
    }
}

Suppose you want to write a function that takes an iterable of numbers and subtracts
the average. That function should be something like:

fn normalise<T: MutIterable<f64>+Collection>(it: &'a mut T) {
  let mean = it.mut_iter().sum() / it.len(); // This would be the same if
                                             //   I used iter instead of
                                             //   mut_iter

  for i in it.mut_iter() {
    *i -= mean;
  }
}

However, this wouldn't work - the compiler wouldn't be able to figure out that &'a mut T implements IterableOwned as I didn't pass a bound. You may want to add a bound in a where clause, something like where &'a mut T: IterableOwned, but the lifetime 'a must be shorter than the lifetime of normalise - as otherwise we would have conflicting borrows of it! - we really want a ∏₁ (universal) lifetime bound - something like ∀'a &'a mut T - but there is no plan for supporting these kind of bounds.

@glaebhoerl glaebhoerl referenced this pull request Sep 14, 2014

Closed

Trait based inheritance #223

@mauro3 mauro3 referenced this pull request in JuliaLang/julia Sep 16, 2014

Open

Interfaces for Abstract Types #6975

@alexcrichton alexcrichton referenced this pull request in rust-lang/rust Sep 16, 2014

Closed

Implement associated items #17307

23 of 26 tasks complete

@alexcrichton alexcrichton merged commit 2c61468 into rust-lang:master Sep 16, 2014

@alexcrichton

This comment has been minimized.

Show comment
Hide comment
@alexcrichton

alexcrichton Sep 16, 2014

Member

This was discussed in last week's meeting and the decision was to merge this.

Member

alexcrichton commented Sep 16, 2014

This was discussed in last week's meeting and the decision was to merge this.

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