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

Add RFC for a more flexible, revised trait matching system #48

Merged
merged 12 commits into from
Jun 10, 2014

Conversation

nikomatsakis
Copy link
Contributor

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:

  1. Full support for proper generic traits ("multiparemeter type classes"),
    including a simplified version of functional dependencies that may
    evolve into associated types in the future.
  2. Generalize explicit self types beyond &self and &mut self etc,
    so that self-type declarations like self: Rc<Self> become possible.
  3. Expand coherence rules to operate recursively and distinguish
    orphans more carefully.
  4. Revise vtable resolution algorithm to be gradual.
  5. Revise method resolution algorithm in terms of vtable resolution.

additional crates are loaded.
- **Crate concatentation:** It should always be possible to take two
creates and combine them without causing compilation errors. This
property
Copy link
Member

Choose a reason for hiding this comment

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

Incomplete sentence. "This property..."?

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

Choose a reason for hiding this comment

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

Unfinished sentence.

@bill-myers
Copy link

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

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?

Copy link
Member

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.

Copy link
Member

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:

Copy link
Member

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

Copy link
Contributor Author

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.,
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 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.

@ben0x539
Copy link

Wouldn't it be nicer to annotate the output types rather than the input types?


// Case 1.
if R <: E:
we're done.

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

Copy link
Member

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.

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.

Copy link
Contributor Author

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

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.

@emberian
Copy link
Member

@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

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

@ben0x539
Copy link

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 trait Whatever<Input> { bit and the output types as associated types among the methods.

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

Choose a reason for hiding this comment

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

Missing backtick

@bill-myers
Copy link

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:

trait Foo<in T, U>
{}

is equivalent to:

trait Foo<T>
{
    type U;
}

It's true that you'll need to invent some syntax to constrain associated types: obvious options are Foo<T>[U = Bar], Foo<T>{U = Bar} or even just Foo<T, U = Bar> (with the latter, I'd suggest putting associated types and generic parameters in the same namespace, so that future keyword-based generic parameters can be added compatibly).

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.

@ghost
Copy link

ghost commented Apr 18, 2014

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 N in the following impl for S:

trait Foo<N: Unsigned + TotalOrd> {
    fn foo(&self) -> N;
}

struct S<F>;

impl<N: Unsigned + TotalOrd, F: Foo<N>> Iterator<F> for S<F> {
    ...
}

Whereas, if output parameters were to become associated types instead, that example would be simpler:

trait Foo {
    type N: Unsigned + TotalOrd;

    fn foo(&self) -> N;
}

struct S<F>;

impl<F: Foo> Iterator<F> for S<F> {
    ...
}

@dobkeratops
Copy link

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'm pretty certain this idea of annotating 'in' is just due to things that are currently ommisions)

trait Dot<V:Vector> {
    type Scalar = type_of( a[0]*b[0] )  ../// if its 'deduced' like this, i can plug in fixed-point types and the shift- value is calculated..
    fn dot(a:&V, b:&V)->Scalar;
}
// ^^^ but will that be insufficient information to satisfy rust? 
// do you want to be able to infer backwards from code that expects a certain scalar type..
// are you still going to need to place type-param bounds on the outputs ?
// would you end up needing to specify both ?

trait Dot<V> {
    type Scalar: TotalOrd  = type_of( a[0]*b[0] ) ;
    fn dot(a:&V, b:&V)->Scalar;
}

@pepp-cz
Copy link

pepp-cz commented Apr 18, 2014

I find the input/output type parameters quite confusing and unnecessary. I am also in favour of associated types.

Edit:
If this proposal is implemented and later the associated types are also implemented, we probably end up using assoc. types instead of the output type parameters but all the remaining type parameters will have to use `in' keyword? I propose that if this RFC is going to happen then the output parameters are "tagged". No matter are much more common they are.

@dobkeratops
Copy link

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

Choose a reason for hiding this comment

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

(truncated sentence)

@alexcrichton
Copy link
Member

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.

@huonw huonw mentioned this pull request Jun 12, 2014
@nikomatsakis nikomatsakis mentioned this pull request Jun 12, 2014
5 tasks
@Centril Centril added A-traits Trait system related proposals & ideas A-typesystem Type system related proposals & ideas A-resolve Proposals relating to name resolution. A-trait-object Proposals relating to trait objects. A-machine Proposals relating to Rust's abstract machine. A-trait-coherence Proposals relating to coherence or orphans. A-method-call Method call syntax related proposals & ideas labels Nov 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-machine Proposals relating to Rust's abstract machine. A-method-call Method call syntax related proposals & ideas A-resolve Proposals relating to name resolution. A-trait-coherence Proposals relating to coherence or orphans. A-trait-object Proposals relating to trait objects. 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.