-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Conversation
cc @glaebhoerl, this is somewhat along the lines of your multidispatch alternative. |
Note that much of this is implemented and waiting for review: rust-lang/rust#16377 |
+100000, this is a massive ergonomics and sanity win.
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):
I could see this getting complicated to resolve though, since you would need to check if
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.
Or am I misunderstanding? (Regardless, I agree that it could be very confusing to use types before they're "declared"). |
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. |
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 ';' |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
+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 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 Some questions:
A few things:
|
println!("{}", u.as_T()) | ||
} | ||
|
||
fn not_allowed(U: Foo)(u: U) { |
There was a problem hiding this comment.
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)
.
+1 from me, this makes generic functions so much better looking in many cases. |
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. |
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:
|
INPUT_PARAM = IDENT [ ':' BOUNDS ] | ||
|
||
BOUNDS = IDENT { '+' IDENT }* [ '+' ] | ||
BOUND = IDENT [ '<' ARGS '>' ] |
There was a problem hiding this comment.
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 }* [ '+' ]
?
Would 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? |
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. |
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;
...
} |
So the where clauses on a trait are basically just a bunch of random claims that must all be satisfied? Is this valid?
Should it be? |
@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. |
The grammar for edit: Oh, they're spelled |
This is an excellent point: I completely overlooked subtraits. I will work out a design and update the RFC.
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
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 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'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.
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
Yes, they are scoped type aliases, which are not allowed today. I will update the RFC with some motivation.
I'll update the RFC with elaboration on these points. |
It turns out that you can constrain associates type synonyms in Haskell, provided that you turn on |
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 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:
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
then it is a violation to even write a trait bound 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 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 Here is an example of what the heck I am talking about:
Here the reference 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). |
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 What GHC doesn't allow, but is present as an example in the RFC, is constraints on top-level
I remember that people have asked about why this isn't allowed, and that sensible reasons were given, but not exactly what they were. |
Sounds to me like Rust people are finally rediscovering the Standard ML module system. |
Having the |
@arielb1 Not really a problem, would query for type equality with |
This will be converted into a crate once associated items are added and used in the `Device` trait. See rust-lang/rfcs#195
This will be converted into a crate once associated items are added and used in the `Device` trait. See rust-lang/rfcs#195
You essentially want to have associated type objects? Something like |
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 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 |
6357402
to
e0acdf4
Compare
This was discussed in last week's meeting and the decision was to merge this. |
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:
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