Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

RFC: associated items and multidispatch #195

Merged
merged 5 commits into from
Sep 16, 2014

Conversation

aturon
Copy link
Member

@aturon 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
Copy link
Member Author

aturon commented Aug 13, 2014

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

@pcwalton
Copy link
Contributor

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

@Gankra
Copy link
Contributor

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

@sfackler
Copy link
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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires adding a keyword?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I'll add a note about that.

@bvssvni
Copy link

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

println!("{}", u.as_T())
}

fn not_allowed(U: Foo)(u: U) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@Kimundi
Copy link
Member

Kimundi commented Aug 13, 2014

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

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

INPUT_PARAM = IDENT [ ':' BOUNDS ]

BOUNDS = IDENT { '+' IDENT }* [ '+' ]
BOUND = IDENT [ '<' ARGS '>' ]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@ben0x539
Copy link

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
Copy link
Member Author

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
Copy link
Member Author

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

@Gankra
Copy link
Contributor

Gankra 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
Copy link
Member Author

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

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
Copy link
Member Author

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
Copy link
Member Author

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

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

@jganetsk
Copy link

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

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

@emberian
Copy link
Member

emberian 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
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
This will be converted into a crate once associated items are added and used in the `Device` trait. See rust-lang/rfcs#195
@arielb1
Copy link
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
Copy link
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.

@alexcrichton
Copy link
Member

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
Labels
A-associated-types Proposals relating to associated types A-syntax Syntax related proposals & ideas A-traits Trait system related proposals & ideas A-typesystem Type system related proposals & ideas
Projects
None yet
Development

Successfully merging this pull request may close these issues.