-
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
Add RFC for a more flexible, revised trait matching system #48
Conversation
additional crates are loaded. | ||
- **Crate concatentation:** It should always be possible to take two | ||
creates and combine them without causing compilation errors. This | ||
property |
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.
Incomplete sentence. "This property..."?
… that to a later RFC
the rules too, but that is work in progress and beyond the scope of | ||
this RFC. Instead, I'll try to explain in "plain English". | ||
|
||
*Note:* I have implemented a |
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.
Unfinished sentence.
Input vs output trait parameters seems highly confusing: is it not allowed to have two impls that differ only on output types? If so, then the proposed syntax is extremely unintuitive and IMHO unacceptable, since the normal way to express that is to have type trait members (aka "associated types"), which is what C++ and Scala use for instance. Otherwise, what's the difference exactly? |
<a name=orphan> *Orphan check*: Every implementation must meet one of | ||
the following conditions: | ||
|
||
1. The trait being implemented (if any) must be defined in the current crate. |
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.
Only current crate? Does it mean a user type can't implement, say, Drop
?
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.
No. That is satisfied by rule 2. The type the trait is being implemented on is an input type parameter, so if the type is defined within the current crate, the implementation will be allowed.
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.
Note condition number 2.
is, by default, the only input type parameters. All explicit type | ||
parameters on a trait default to *output* unless prefixed by the `in` | ||
keyword. This means that, e.g., the trait `Add` should be defined as: | ||
|
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.
As an alternative, could we use a tuple of types for Self? I.e., allow trait Add<LHS, RHS, SUM>: (LHS, RHS)
(or something to bound Self
to tuples) and impl Add<int, int, int> for (int, int)
, etc. I'm sure we can do better than this rather verbose first attempt.
I guess this is just an alternate syntax in the end. I would prefer to use an existing language feature than add a new keyword (well, use an existing keyword in a new way). Also I find the use of in
here confusing since it is not directly related to the usual meaning of input/output parameters on regular (not type) parameters. E.g, I could have a function which maps T -> T
where a type parameter is the type of both input and output parameters (assuming the second use of T
is an out param, not a return type).
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.
On Wed, Apr 16, 2014 at 01:53:00PM -0700, Nick Cameron wrote:
As an alternative, could we use a tuple of types for Self? I.e.,
allowtrait Add<LHS, RHS, SUM>: (LHS, RHS)
(or something to bound
Self
to tuples) andimpl Add<int, int, int> for (int, int)
,
etc. I'm sure we can do better than this rather verbose first
attempt.
I thought of this and dismissed it for some reason. Maybe because I
thought input type parameters would come up more often. But I've not
found them in practice almost anywhere in the standard library besides
the operator traits. Given the coherence rules I was planning to use,
I think tuples would work, so that might be an appealing
simplification.
Wouldn't it be nicer to annotate the output types rather than the input types? |
|
||
// Case 1. | ||
if R <: E: | ||
we're done. |
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.
Perhaps I am missing something, but, as written here, wouldn't case 1 spuriously accept a reference in a method that requires a value parameter (since R
was derefed in METHOD-SEARCH
)?
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.
I don't believe it will. Note that the only subtyping is with regions, so R and E must be the same type, excepting their lifetimes.
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.
I meant that, since R
has been derefed by the time it gets to RECONCILE
, R
would be the same as E
.
e.g.:
struct Foo { ... }
trait Bar {
fn consume(self) { ... }
}
impl Bar for Foo { ... }
// should be an error, unless Foo is Copy
fn consume_foo(foo:&Foo) {
foo.consume();
}
METHOD-SEARCH
will deref &Foo
to Foo
(since that what Bar
is defined on), and calls RECONCILE(Foo, Bar, m)
. E
will be Foo
. Foo <: Foo
, so case 1 applies.
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.
On Wed, Apr 16, 2014 at 03:00:29PM -0700, zkamsler wrote:
I meant that, since 'R' has been derefed by the time it gets to
RECONCILE
,R
would be the same asE
.e.g.:
struct Foo { ... } trait Bar { fn consume(self) { ... } } impl Bar for Foo { ... } // should be an error, unless Foo is Copy fn consume_foo(foo:&Foo) { foo.consume(); }
METHOD-SEARCH
will deref&Foo
toFoo
(since that whatBar
is defined on), and callsRECONCILE(Foo, Bar, m)
.E
will beFoo
.Foo <: Foo
, so case 1 applies.
Yes. This is all correct. However, what you are missing is that just
because method-search succeeds doesn't mean that all other type checks
succeed. The borrow checker would then later flag this as an error as
a move out of a borrowed location.
@ben0x539 no, because being an input is going to be relatively rare. Marking the inputs reduces annotation burden. |
necessarily `Self`) must meet the following grammar, where `C` | ||
is a struct or enum defined within the current crate: | ||
|
||
T = C |
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 seems to need at least one more space to render in markdown as a code block nested inside the enumeration.)
If I understand associated types in type families correctly, they basically wouldn't show up in the type parameters to the trait/type class, just among the methods. Maybe I'm getting ahead of ourselves here, but if we do get associated items I think it would also make our 'multi-param' traits declaration easier to visually parse to have the input types unannotated right there in the Anyway, this all looks very exciting :) |
implement `Add` for `int`. However, the type `RHS` is different in | ||
each case, and `RHS` is an input type parameter. | ||
|
||
Note that it is crucial for `RHS to be an input type parameter. |
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.
Missing backtick
As far as I can tell, output type parameters in this RFC are completely equivalent to associated types, except for different syntax and the fact that associated types are identified by name and not parameter position. That is:
is equivalent to:
It's true that you'll need to invent some syntax to constrain associated types: obvious options are This syntax would of course be usable with trait objects too. Some syntax to refer to them both from types and values is also needed, such as X::U from C++, and either value.U or something like typeof(value)::U. I think that the associated type syntax is far more intuitive, since it's obvious that you can only implement the trait once regardless of output types, while you need to read the language manual to figure out what "in" means and to realize that despite lack of type erasure, parameters are "out" by default ("in" may be confused for a variance annotation, for instance). Also, it distinguishes between "input" and "output" parameters in trait uses in addition to declarations, making it easier to read code. The special associated type constraint syntax would be unique to Rust, but also IMHO relatively intuitive and especially hard to misinterpret. |
I agree with bill-myers on output parameters being better served as associated types. A problem with both the current design and this proposal is that you end up having to repeat a lot of bounds like the ones for
Whereas, if output parameters were to become associated types instead, that example would be simpler:
|
Are you going to be able to work the same way as in C++ where you can just specify input types, and deduce the output types - or would that not play well with the 2-way inference.
|
I find the input/output type parameters quite confusing and unnecessary. I am also in favour of associated types. Edit: |
this might be tangential but i'm asking after seeing the talk of generalizing the self parameter - and the ideas about assoc/static functions being changed - Would it be possible for UFCS to go as far as eliminating the difference between a.foo(b) and foo(a,b) for all functions - a.foo would simply be sugar for putting a parameter at the front which is convient for chaining without nesting, and for IDE's giving suggestions - decouple that from anything to do with polymorphism or visibility, availalbe to all functions equally. To provide a sane roadmap my suggestion would be that you still only use the first parameter for vtables ; 'Self' is just a shortcut for 'the current type in the impl block' and nothing special, the function goes in the vtable if the first parameter is of the self type. Leave the door open for generalized multimethod dispatch in future, moving conceptually away from the concept of methods being something distinct from functions.. "methods are just functions that happen to have been pulled into a vtable" |
## Why functional dependencies and not traits implemented over tuples? <a name=tuples> | ||
|
||
All things being equal, I might prefer to say that only the `Self` | ||
type of a trait is |
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.
(truncated sentence)
As discussed in today's meeting, we have decided to merge this. The controversial bits have been left out, and what was renaming was unanimously agreed upon. |
Summary
Cleanup the trait, method, and operator semantics so that they are
well-defined and cover more use cases. A high-level summary of the
changes is as follows:
including a simplified version of functional dependencies that may
evolve into associated types in the future.
&self
and&mut self
etc,so that self-type declarations like
self: Rc<Self>
become possible.orphans more carefully.