Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upRFC: associated items and multidispatch #195
Conversation
This comment has been minimized.
This comment has been minimized.
|
cc @glaebhoerl, this is somewhat along the lines of your multidispatch alternative. |
This comment has been minimized.
This comment has been minimized.
|
Note that much of this is implemented and waiting for review: rust-lang/rust#16377 |
This comment has been minimized.
This comment has been minimized.
|
+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"). |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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. |
huonw
reviewed
Aug 13, 2014
| ... <existing productions> | ||
| | 'static' IDENT ':' TYPE [ '=' CONST_EXP ] ';' | ||
| | 'type' IDENT [ ':' BOUNDS ] [ WHERE_CLAUSE ] [ '=' TYPE ] ';' | ||
| | 'lifetime' LIFETIME_IDENT ';' |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
huonw
reviewed
Aug 13, 2014
| } | ||
| } | ||
| impl<K,V> HashMap<K,V> where K: ContainerKey { |
This comment has been minimized.
This comment has been minimized.
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.
This comment has been minimized.
huonw
reviewed
Aug 13, 2014
| // as implemented by `Vec<T>` | ||
| ``` | ||
|
|
||
| ### Ways to reference items |
This comment has been minimized.
This comment has been minimized.
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.
This comment has been minimized.
aturon
Aug 13, 2014
Author
Member
I believe they should work identically to type paths. I'll add details to the RFC soon.
huonw
reviewed
Aug 13, 2014
| } | ||
| ``` | ||
|
|
||
| Note that `Vec<T>::E` and `Vec<T>::empty` are also valid type and function |
This comment has been minimized.
This comment has been minimized.
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?)
This comment has been minimized.
This comment has been minimized.
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 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:
|
bvssvni
reviewed
Aug 13, 2014
| println!("{}", u.as_T()) | ||
| } | ||
| fn not_allowed(U: Foo)(u: U) { |
This comment has been minimized.
This comment has been minimized.
bvssvni
reviewed
Aug 13, 2014
| 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.
This comment has been minimized.
huonw
reviewed
Aug 13, 2014
|
|
||
| The proposed scoping rule is: | ||
|
|
||
| * Associated types are in scope for the trait body |
This comment has been minimized.
This comment has been minimized.
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
reviewed
Aug 13, 2014
| 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.
This comment has been minimized.
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
reviewed
Aug 13, 2014
| let f = Foo { ... }; | ||
| let b = Bar { ... }; | ||
| let mut v = Vec::new(); | ||
| v.push(box f as Show); |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
+1 from me, this makes generic functions so much better looking in many cases. |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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:
|
ben0x539
reviewed
Aug 13, 2014
| INPUT_PARAM = IDENT [ ':' BOUNDS ] | ||
| BOUNDS = IDENT { '+' IDENT }* [ '+' ] | ||
| BOUND = IDENT [ '<' ARGS '>' ] |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
ben0x539
commented
Aug 13, 2014
|
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? |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
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;
...
} |
This comment has been minimized.
This comment has been minimized.
|
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? |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
ben0x539
commented
Aug 13, 2014
|
The grammar for edit: Oh, they're spelled |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
It turns out that you can constrain associates type synonyms in Haskell, provided that you turn on |
This comment has been minimized.
This comment has been minimized.
|
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). |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
jganetsk
commented
Aug 31, 2014
|
Sounds to me like Rust people are finally rediscovering the Standard ML module system. |
This comment has been minimized.
This comment has been minimized.
|
Having the |
This comment has been minimized.
This comment has been minimized.
|
@arielb1 Not really a problem, would query for type equality with |
brendanzab
added a commit
to brendanzab/gfx-rs
that referenced
this pull request
Sep 1, 2014
brendanzab
referenced this pull request
Sep 1, 2014
Merged
Move OpenGL device implementation into separate directory #341
brendanzab
added a commit
to brendanzab/gfx-rs
that referenced
this pull request
Sep 1, 2014
This comment has been minimized.
This comment has been minimized.
|
You essentially want to have associated type objects? Something like |
This comment has been minimized.
This comment has been minimized.
|
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 |
nrc
assigned
aturon
Sep 4, 2014
alexcrichton
referenced this pull request
Sep 8, 2014
Closed
Type aliases and mods of the same name in the same namespace should be forbidden #6936
alexcrichton
force-pushed the
rust-lang:master
branch
from
6357402
to
e0acdf4
Sep 11, 2014
lilyball
referenced this pull request
Sep 15, 2014
Merged
RFC: conventions for placement of unsafe APIs #240
alexcrichton
merged commit 2c61468
into
rust-lang:master
Sep 16, 2014
This comment has been minimized.
This comment has been minimized.
|
This was discussed in last week's meeting and the decision was to merge this. |
aturon commentedAug 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:
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
implis selected based on multiple types. The connection to associated items will
become clear in the detailed text below.
Rendered view