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

Where clauses for more expressive bounds #135

Merged
merged 5 commits into from
Sep 30, 2014

Conversation

nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented Jun 24, 2014

Add where clauses, which provide a more expressive means of specifying trait parameter bounds. A where clause comes after a declaration of a generic item (e.g., an impl or struct definition) and specifies a list of bounds that must be proven once precise values are known for the type parameters in question.

Main benefits:

  • Express bounds on types derived from type parameters, e.g. Option<T> where T is a type parameter.
  • Permits true multiple dispatch.

Rendered view.

@thehydroimpulse
Copy link

The bounds syntax was definitely an issue when working with things like encoders and decoders, it makes things pretty hard to read.

The fact that this also allows multiple dispatch is pretty awesome, so +1. The associated type syntax for specifying bounds is a little bit weird, but it makes sense the first time looking at it. I also definitely prefer the where keyword instead of some cryptic character.

Are associated types going to have a separate RFC?

@bill-myers
Copy link

It seems it wouldn't be that bad to just use the same syntax to both declare type parameters and bounds on arbitrary types.

That is:

struct Foo<K, V: Clone, Option<V>: MyTrait> {}

For the formatting issue one could write it ilke this

template<K, V: Clone, Option<V>: MyTrait>
struct Foo {}

Which also allows to reuse types for multiple declarations, like this (this has been suggested in the past):

template<K, V: Clone, Option<V>: MyTrait>
{
    struct Foo {}
    struct Bar {}
}

Instead of "template", one could use "where" or perhaps "type", or even bare angle brackets with no keyword at all, like this:

<K, V: Clone, Option<V>: MyTrait>
{
    struct Foo {}
    struct Bar {}
}

If nesting is allowed, then there is a slight ambiguity, since something like this:

<K, V: Clone, Option<V>: MyTrait>
{
    struct Foo {}
    <K: Eq>
    {
        struct Bar {}
    }
}

Could be interpreted as either declaring an additional K parameter, or adding an Eq bound to K.

However, hiding an existing parameter doesn't seem very useful, so interpreting this as adding a bound to K should solve the issue.

@glaebhoerl
Copy link
Contributor

@bill-myers that was #122

@lilyball
Copy link
Contributor

👍 I like this a lot.

In such cases, the bound will be checked in the callee, not the
caller, and is not included in the list of things that the caller must
prove. Therefore, given the following two functions, an error
is reported in `foo()`, not `bar()`:
Copy link
Contributor

Choose a reason for hiding this comment

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

Why should this even be allowed? Are there any benefits to testing the bounds in the callee, vs just reporting an error that the where clause does not refer to any type parameters?

@nikomatsakis
Copy link
Contributor Author

On Tue, Jun 24, 2014 at 09:19:39AM -0700, bill-myers wrote:

Wouldn't it be unambiguous to just use the same syntax to both declare type parameters and bounds on arbitrary types?

It is to some extent, yes. I suppose the only thing it rules out are
the "trivial" bounds like "int : Eq". But I find it a bit odd that
we'd be stuffing things into the <...> list that will not later
appear when referencing the type.

For the formatting issue one could write it ilke this

template<K, V: Clone, Option<V>: MyTrait>
struct Foo {}

Speaking personally, I've always found the C++ style confusing because
when you reference the type you write Foo<...> but when you
define it, you write template<...> struct Foo { ... }. Perhaps
it's just me, but I never seem to get used to this.

@ben0x539
Copy link

Wow that's a tricky multi-dispatch pattern. The original, "more complicated multidispatch proposal" looks a lot cuter without the duplication of the input types, and more generally with putting all the input parameters in a tuple and having only the output parameters show up in the <>.

(No comments on the actual where proposal, I can't imagine your latest round of RFCs is too controversial.)

@lilyball
Copy link
Contributor

Yeah, multi-dispatch is complicated, but I'm glad it's possible. And this would presumably open the door to making it simpler to define multi-dispatch methods later (even if it desugars into this complicated version).

fn reduce<T:Clone>(xs: &[T]) -> T
where () : Add<T,T,T>
// ^~~~~~~~~~~~~~~ Note: DRY
{
Copy link
Member

Choose a reason for hiding this comment

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

This is a bit unimportant, but I don't see the advantage with this idiom - is the only benefit that you don't have to write T,T on the lhs here? It seems like a lot of work to go to for that and the result seems less clear to me. Does it facilitate the next step in some way?

+1 For the proposal in general, looks good.

Copy link
Contributor

Choose a reason for hiding this comment

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

AIUI using () like this is just for DRY, because you only have to write the impl on () once, but every time you need to bound on Add you have to write it out, so saying where (L,R) : Add<L,R,S> is repetitious.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at this a bit more, I'm confused. The trait coherence rules documented above say that you can't implement the same trait twice on a given type, even if the type parameters to the trait differ. But here you're defining multiple Add implementations on the single type (). Isn't that a violation of the same coherence rule that prevented Add<Complex, Complex> + Add<int, Complex> on Complex?

And if you intended to relax the coherence rules to allow this, then what's the point on defining the trait on (L,R) to begin with? You could just implement it directly on ().

The only difference in the impl on () seems to be the fact that it's implemented in terms of type parameters, but I don't see why that should make a difference. And if it is somehow special, then what is the type of the expression <() as Add>::add?

Choose a reason for hiding this comment

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

But there's only one impl for (), it's just really generic and only does the dispatching to the regular impls that are on various different tuple types and thus coherent.

<() as Add>::add probably still needs to infer that it's really <() as Add<T, T, T>>::add (the really-generic impl on ()), which then calls <(T, T) as Add<T, T, T>>::add (one of the explicit impls like mpl Add<int,int,int> for (int,int) { ... }). I think it's really just to avoid spelling out <(T, T) as Add>, an aid to point inference in the right direction.

This scheme of leaving the dispatch to a helper function/impl also seems to lock out gimmicky impls like Add<T, U, V> for (A, B) with A, B != T, U since the () impl basically doublechecks that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, so the trait coherence rules actually only care about the number of impl blocks, not the number of concrete implemented traits? Interesting. And it looks like you're right, I can verify that in a quick test. Still, seems pretty odd. And I still want to know how <() as Add>::add is supposed to be typed, because <() as Add<T, T, T>>::add is not a type, it's an expression. I guess it would just be considered an unconstrained type though, preventing it from being inferred properly.

I thought about suggesting the syntax <() as Add<L,R,S>>::add, which would need to be a modification to the UFCS proposal (I glanced over it, but didn't see that in there, all the examples left the trait unparameterized, as in <() as Add>). I'm not sure if that's particularly useful though.

In any case, given this rule for trait coherence, it seems like we can skip the tuple stuff altogether. Instead do something like

// in mod ops
mod impl {
    pub trait Add<LHS,RHS,Result> {
        fn add(lhs: &LHS, rhs: &RHS) -> Result;
    }
}

pub trait Add<RHS,Result> {
    fn add(&self, rhs: &RHS) -> Result;
}

impl<LHS,RHS,Result> Add<RHS,Result> for LHS
where (LHS,RHS) : impl::Add<LHS,RHS,Result>
{
    fn add(&self, rhs: &RHS) -> Result {
        <(Self,RHS) as impl::Add>::add(self, rhs)
    }
}

With this, you can now just say

use AddImpl = core::ops::impl::Add;

impl AddImpl<Complex, int, Complex> for (Complex, int) { ... }
impl AddImpl<Complex, Complex, Complex> for (Complex, Complex) { ... }
impl AddImpl<int, Complex, Complex> for (int, Complex) { ... }

This looks similar to the Add impls in the RFC, but by using split traits like this, we can skip the () nonsense. Usage would now just look like

fn reduce<T:Clone + Add<T,T>>(xs: &[T]) -> T {
    let mut accum = xs[0].clone();
    for x in xs.slice_from(1) {
        accum = accum.add(&x);
    }
}

This also skips the need for a global function. And in fact all existing code that references Add<RHS,Result> bounds will continue to work. The only change is the implementation of the trait.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah right, the tuple being a reference itself is not right. But yeah, using (&Complex, &int) would work.

As for referring to the left- or right-hand side individually, I suppose if you actually need to do that, then use the Add<L,R,S> pattern. But I don't see the benefit to using that for traits that don't need to refer to the LHS or RHS in the trait definition (like Add). With the approach I outlined, Add<S> vs Add<L,R,S> is just an implementation detail, and users always say Add<R> when using it as a type bound.

Choose a reason for hiding this comment

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

With where clauses, I'm tempted to try to construct some sort of ad-hoc type destructuring device like this:

mod detail {
    pub trait IsSameType {}
    impl<T> IsSameType for (T, T) {}
}

pub trait Add<Result> {
    fn add<T, U>(&T, &U) -> Result
        where ((T, U), Self) : detail::IsSameType;
}

but I'm not sure whether it's actually possible to implement that method anymore. ;)

Copy link
Contributor

Choose a reason for hiding this comment

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

If where clauses are extended to support basic == comparison of types (which I think will be necessary if we get associated types), then you can do destructuring like

impl<T,L,R> Foo<T>
where T == (L,R)
{ ... }

Of course, this isn't very useful as written, because that's equivalent to just using (L,R) in place of T:

impl<L,R> Foo<(L,R)> { ... }

But that's an impl block. Presumably the need to get at the component types of the tuple is if they're needed in a type signature for one of the trait functions. In that case, if we had a way of introducing type parameters in a trait block that aren't actually considered parameters to the trait name, that would work. Either of the following two syntaxes could represent that:

pub trait<L,R> Foo<T>
where T == (L,R)
{ ... }

or

pub trait Foo<T>
where <L,R> T == (L,R)
{ ... }

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 Tue, Jun 24, 2014 at 02:28:40PM -0700, Kevin Ballard wrote:

Looking at this a bit more, I'm confused. The trait coherence rules documented above say that you can't implement the same trait twice on a given type, even if the type parameters to the trait differ. But here you're defining multiple Add implementations on the single type (). Isn't that a violation of the same coherence rule that prevented Add<Complex, Complex> + Add<int, Complex> on Complex?

There is exactly one impl for (), so there is no coherence
violation. However, over the last day or so, I did realize that the
proposal doesn't work quite how I described it: I'd have to change the
rules regarding so-called "trivial" obligations that do not involve
type parameters. You still want the caller to verify those
obligations in order to use the

() : Trait<A,B,C>

style. This is because, not knowing the types A and B, the callee
doesn't have enough information to completely resolve the impl. This
is fine. (Simpler, really.)

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 Tue, Jun 24, 2014 at 03:48:51PM -0700, Kevin Ballard wrote:

Actually even this is unnecessarily repetitious. The Add impl trait has no need to be parameterized on the LHS or RHS, as those are both part of the tuple type it's implemented on.

It is true that one could define Add simpler, though it'd be a
semantic change from today since it would have to take its left and
right arguments by value and not by reference (since the argument must
be a (L,R) tuple, not (&L,&R)). But I wanted to define a pattern
that scales well to the more general case.

@bstrie
Copy link
Contributor

bstrie commented Jun 24, 2014

I'm exceptionally wary of having two completely different ways of doing the same thing. Not only will we have to specify all the ways in which they interact with each other, but future enhancements will have to consider which methods will support which features. For instance, what happens with the following?

fn foo<T: Eq>(x: T) where T: Ord { ... }

I'm curious how onerous it would be to get rid of the old syntax and entirely replace it with where.

And if we do decide to keep both, at what threshold of complexity is one supposed to stop using <T: Foo> and start using where T: Foo?

@lilyball
Copy link
Contributor

@bstrie With a where clause, the constraints inside of the <> are just syntactic sugar. So <T: Foo> is just sugar for <T> where T: Foo. It seems pretty straightforward.

Threshold of complexity would be up to the opinion of whomever is writing the code. But we already have that issue today, e.g. how complex must a generic type specialization be before you give it a type alias?

@ftxqxd
Copy link
Contributor

ftxqxd commented Jun 25, 2014

👍 By a strange coincidence, yesterday I was thinking of something exactly like this. However, there is still a major problem that is only partially fixed by this. Type parameters are really three things: type variable declarations, type parameters, and type bounds. Right now these are (in most cases) combined into one (with type parameters separate on impl blocks). This RFC aims to separate type bounds from the rest, but still doesn’t address the issue of type declarations and type parameters being inseparable. This is an issue because a struct like struct Foo<T, I: Iterator<T>> { it: I } doesn’t use the type parameter T anywhere in its fields, and therefore requires redundancy when referring to it (e.g. Foo<char, Chars>).

To remedy this I’d probably make all type declarations implicit, which solves the issue of separating type variable declarations from type parameters: struct Foo<I: Iterator<T>> { it: I } would work and wouldn’t need an unnecessary type parameter T. This also allows the alternative for multiple dispatch (trait Add<S> for (L, R) in the RFC) to work with no special syntax:

use std::tuple::Tuple2;

trait Add<S>: Tuple2<L, R> {
    fn add(left: &L, right: &R) -> S;
}

impl Add<int> for (int, int) {
    fn add(left: &int, right: &int) -> int { ... };
}

fn reduce<T: Clone>(xs: &[T]) -> T
        where (T, T): Add<T> {
    let mut accum = xs[0].clone();
    for x in xs.slice_from(1) {
        accum = <() as Add>::add(&accum, &x);
    }
}

(An alternative would be to require explicit declaration of type variables everywhere, resulting in verbosity like struct<A, B> Foo<A, B> where A: X, B: Y { … }.)

This means that there are no hacky workarounds using (), so the code is a lot more understandable. This also means that the impls too are DRY.

(The above proposal is probably best suited for an RFC, but would only make sense after this RFC is accepted.)


Another thing I’d like to note is that I think the proposed Add trait would be able to be implemented with UFCS even without this proposal (albeit with a few redundant type parameters on functions (which could be resolved with my suggestion above)):

trait Add<L, R, S> {
    fn add(left: &L, right: &R) -> S;
}

fn reduce<T: Clone, A: Add<T, T, T>>(xs: &[T]) -> A {
    let mut accum = xs[0].clone();
    for x in xs.slice_from(1) {
        accum = <A as Add>::add(&accum, &x);
    }
}

}
}

Now the expression `a + b` would be sugar for `Add::add((a, b))`.

Choose a reason for hiding this comment

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

This would also represent a change from pass-by-shared-reference to pass-by-value for operators. Have you given it some thought whether there is a way to support both variants? For some types pass-by-shared-reference might be more efficient. With settling on tuples it seems to be even harder to get the best of both parameter passing styles.

Copy link
Contributor

Choose a reason for hiding this comment

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

I expect this is because this is just an example of an alternative implementation and is not being proposed as how Add should actually work. If it were, it would presumably actually end up being Add::add((&a, &b))

@bstrie
Copy link
Contributor

bstrie commented Jun 25, 2014

With a where clause, the constraints inside of the <> are just syntactic sugar. So <T: Foo> is just sugar for where T: Foo. It seems pretty straightforward.

@kballard, I don't see this mentioned in the RFC anywhere. I would like to see it explicitly specified.

Furthermore, my question was whether we would allow the intermixing of these features. I'd be more inclined to say that if you use a where clause on a function at all, then you must move all bounds out of the typaram list and into the where clause. If you forget, then a syntax error would result.

@lilyball
Copy link
Contributor

@bstrie The RFC does already demonstrate that constraints inside the <> can be moved into the where clause, such that the following are identical:

func foo<T: Clone>(x: T) -> T;
func foo<T>(x: T) -> T where T: Clone;

Furthermore, my question was whether we would allow the intermixing of these features.

Why not? And the RFC already contains examples of code that use both forms in the same declaration. The only thing it doesn't demonstrate is putting bounds on the same type in both places, e.g.

func foo<T: Clone>(x: T) -> T where T: Hash;

and I see no reason to prohibit that.

@bstrie
Copy link
Contributor

bstrie commented Jun 25, 2014

I see no reason to prohibit that.

My personal aversion to TIMTOWTDI is my rationale for prohibiting it (if indeed we decide not to remove it entirely).

But beyond matters of taste, it's also the safer option. We can make rules strict now and loosen them later. Vice versa is not possible.

@dobkeratops
Copy link

Would it be better to just allow overloading on multiple types, and check these functions when called. Consider the error messages& bounds a separate problem, to be solved independantly;
vtable/trait objects need only consider the first parameter.

You could open up lots of of functionality in the language, and defer improvements to error-mesages for library code.

Library code could just stick to using indirection-traits, if you think errors are the most important issue.

IMO
I think the extra complexity in trait bounds is a double edge sword. I really worry what this will end up looking like with HKT as well.

It's a shame because this is orthogonal to Rusts' core pillars.
On the one hand, traits are a step forward over C++ because it gives us extension methods, but on the other, losing overloading introduces extra hoops to jump through.

The problem with C++ is not 'having lots of features' - its features that are half broken so you have to go through contortions to work around them. And the need for backward compatibility means, we can't fix them.

Indirection traits or having to destructure 'self' to access lhs/rhs seem to me like contortions resulting from missing overloading. It seems we can overload multiple parameters by building an argument tuple, but it look strange.
The "a.foo(b)" call syntax is useful for readability (approximates infix notation, and plays well with IDE's) and in my ideal world it would have nothing to do with access rights or polymorphism.. it would be a completely orthogonal calling sugar.

IMO the problem in C++ is headers, which worsen overloading/templates (i.e. needing the right headers in the right order before something will compile). Given that Rust doesn't have headers, it wouldn't suffer from this problem;
..and you would have significantly cut down on the burden of error messages with the cases that trait bounds do handle. Error messages' aren't as critical an issue as safety for rusts' "core mission".

When it does come to specifying a bound on a group of functions reliant on multiple types, the concept of a "Self" might get in the way. What if you want to group together 3 types, and some functions use different pairs. (a matrix, a vector, a scalar). How this all works with standalone generic functions is very clear. Traits make it harder.

@nikomatsakis
Copy link
Contributor Author

On Tue, Jun 24, 2014 at 03:42:19PM -0700, Ben Striegel wrote:

I'm exceptionally wary of having two completely different ways of
doing the same thing. Not only will we have to specify all the ways
in which they interact with each other, but future enhancements will
have to consider which methods will support which features.

Since as how the old bounds are syntactic sugar for where bounds, I
am not considered about complexity with regard to the semantic
meaning, but I am generally sympathetic to the idea that we should
have exactly one syntax.

For instance, what happens with the following?

fn foo<T: Eq>(x: T) where T: Ord { ... }

There is just one set of constraints and they are unioned together, so there is no problem here.
That is, the function above is equivalent to:

fn foo<T>(x: T) where T:Ord, T:Eq { ... }

(There are other equivalent ways one could write the declaration as well.)

I'm curious how onerous it would be to get rid of the old syntax and entirely replace it with where.

Yeah, I don't know.

And if we do decide to keep both, at what threshold of complexity is one supposed to stop using <T: Foo> and start using where T: Foo?

Speaking personally, it'd probably be "any bound more complicated than
a single identifier".

@nikomatsakis
Copy link
Contributor Author

On Fri, Jun 27, 2014 at 05:01:35AM -0700, dobkeratops wrote:

Would it be better to just allow overloading on multiple types, and
check these functions when called. Consider the error messages a
separate problem, to be solved in parallel; vtable/trait objects
need only consider the first parameter.

I don't know precisely what you mean, to be honest. I think you mean
redefine the "output" type parameters on the trait as inputs
(perhaps optionally). That would mean that the Add trait could be
defined as today but making the type of the RHS an "input" that is
used to select the impl. However, it would very seriously harm our
ability to infer types in many common situations, unless we added some
syntactic category to distinguish the input from output types. Having
though it over some, I strongly prefer not doing that and prefer to
overload based on a tuple of types. I think the overall model is
simpler this way.

@gasche
Copy link

gasche commented Jun 30, 2014

For an outsider it looks like a great proposal. In particular, it would solve the issue discussed in my comment on trait synonyms.

From a historical/academic perspective, the first occurence of where <constraint> I know about is the 2005 paper Generalized Algebraic Data Types and Object-Oriented Programming by Andrew Kennedy and Claudio Russo. They remark that the counterpart of GADTs in object-oriented languages is precisely the ability to add "where constraints" on the types of some methods of the class. Interestingly, this is actually needed in much more cases than you would expect (GADTs being a rather arcane feature); if you want to have a flatten : List<List<T>> -> List<T> method in the List class itself, you need such where-constraints. In this paper, the constraints are only equality between types, but it was generalized to arbitrary subtyping constraints in 2006.

@UtherII
Copy link

UtherII commented Jul 1, 2014

I like the syntax proposed by gashe.
It doesn't introduce a new keyword and It seem more natural to me to see constraints on types parameters before their first appearance.

@glaebhoerl
Copy link
Contributor

As far as the actual feature being proposed is concerned, which is where clauses, I like it.

The whole discussion about how to encode "multidispatch traits", on the other hand, gives me the heebie jeebies. Compared to the beautifully simple MPTCs of Haskell? Seriously?

Instead of using more gunk to accomodate existing conceptual gunk, I would rather go to work on making the existing features simpler, more consistent, and more accommodating.

The following is off the top of my head, so the details can probably stand wibbling, but e.g.:

  • Remove the implicit Self parameter and self.

    A trait formerly written

    trait Foo { 
        fn foo(&self);
    }
    

    is now written

    trait Foo<T> { 
        fn foo(arg: &T); 
    }
    
  • The syntax T: Foo is now just syntactic sugar for Foo<T>, so e.g.

    fn bar<T: Foo>() { ... }
    

    is the same as

    fn bar<T>() where T: Foo { ... }
    

    is the same as

    fn bar<T>() where Foo<T> { ... }
    
  • All listed type parameters of trait declarations are "input type parameters", used when selecting the matching impl.

  • All "output type parameters", chosen by the impl, are associated types.

This preserves "there's one way to do it" on the semantic level, avoiding the whole TFs vs FDs debate Haskell has been laboring under (and also avoiding the need to figure out some inevitably-awkward syntax for FDs), and matches intuition, i.e. the things defined by the impl are all listed in the body of the trait.

The Add example is now simply:

trait Add<LHS, RHS> {
    type Result;
    fn add(lhs: &LHS, rhs: &RHS) -> Result;
}

with no funny business required.

@sanxiyn
Copy link
Member

sanxiyn commented Jul 2, 2014

where clause seems uncontroversial, and (seemingly, unlike other commenters here) I like "trait over a tuple of types" solution to multiple dispatch.

I first encountered the idea of using a tuple for multiple dispatch from PyPy. Here is a link to PyPy's pairtype.

@ghost
Copy link

ghost commented Jul 2, 2014

The following example of yours is unreadable mainly because of the code formatting style you use:

fn set_var_to_merged_bounds<T:Clone + InferStr + LatticeValue,
                              V:Clone+Eq+ToStr+Vid+UnifyVid<Bounds<T>>>(
                              &self,
                              v_id: V,
                              a: &Bounds<T>,
                              b: &Bounds<T>,
                              rank: uint)
                              -> ures;

After editing only whitespaces:

fn set_var_to_merged_bounds
    <
        T : Clone + InferStr + LatticeValue,
        V : Clone + Eq + ToStr + Vid + UnifyVid<Bounds<T>>
    >
    (
        &self,
        v_id : V,
        a : &Bounds<T>,
        b : &Bounds<T>,
        rank : uint
    )
    -> ures;

+4 lines, but the readability easily wins the trade-off IMO.

@ben0x539
Copy link

ben0x539 commented Jul 2, 2014

The unreadable version is the recommended style, though. https://github.com/rust-lang/rust/wiki/Note-style-guide#function-declarations

@glaebhoerl
Copy link
Contributor

One more thing:

One interesting point is what to do for "trivial" where clauses where
the self-type does not refer to any of the type parameters:

fn foo()
    where int : Eq
{ ... }

Unless there's an obvious theoretical justification for it to work a certain way (i.e. for logical consistency with other cases), I think the prudent thing to do here, given that the use case is not obvious (and neither, therefore, is how it should work), would be forbid it. Then when someone files a bug report or RFC saying, "I want to be able to write this because of reason", we'll have a better idea of why it should do what, and we can implement it that way. In other words, don't commit to a semantics prematurely.

(In Haskell, it checks whether there is an instance in scope at the call site, as for all other constraints. This makes sense for Haskell because you can have orphan instances, but it may not for Rust.)

@lilyball
Copy link
Contributor

lilyball commented Jul 2, 2014

@glaebhoerl Why restrict that, though? It seems like it should work just fine without the restriction, and I don't see any benefit to adding that restriction. I could see perhaps a lint that warns you that your where-clause is asserting something about a type that that is not using type parameters, as that may be a mistake, but that's different than outright forbidding it.

@glaebhoerl
Copy link
Contributor

@kballard Because it's not clear whether it should be checked at the callee, the caller, or whatever. If it is clear and there's only one obvious way it could work, then my comment was based on invalid assumptions and should be disregarded. But if there's more than one possible choice, and we arbitrarily choose one of them, and then we later discover a reason for doing it the other way, it's a backwards compatibility break. If we start out not allowing it, then we're free to add it later in either form. (I assumed the choice was arbitrary because the RFC didn't provide any justification for it, but again, that may not be accurate.)

@Ericson2314
Copy link
Contributor

@glaebhoerl I could not agree with you more about getting rid of Self in traits and what not. My guess to why that hasn't been done long ago is object types. But IMO people will eventually be clamoring for more powerful existentials anyways, and replacing the current syntax with e.g. exists<T> T where Add<T,Int> would also remove the need for the <T as Trait> syntax in UFCS.

I am hoping the "tuple trick" was just an example of what could be done with this new syntax to fake mulch-parameter traits as a stop gap, and eventually we will get the real thing, hopefully as @glaebhoerl describes it.

Also +1 for disallowing the current bound syntax if this lands. In addition to the other reasons mentioned, If we get HKTs, kind signatures would take that syntax, and we'd have to refactor our codes anyways. Better refactor now pre-1.0 than later.

@glaebhoerl
Copy link
Contributor

@Ericson2314 I was thinking kind signatures would have a prefix form-follows-declaration and vaguely C++-like syntax, e.g. type T (int), type<type> T (Option), type<type, type> T (Result), static N: uint, etc. So the conflict is avoidable.

@Valloric
Copy link

Valloric commented Jul 6, 2014

This RFC is a great idea, but the existing trait bounds syntax should then be removed. "There should be one and only one obvious way to do it." If you add two ways to do something, then:

  • You've just forced large teams to add rules to their style guide to enforce consistency.
  • Everyone needs to learn both ways of doing something (thus unnecessarily increasing cognitive load).
  • You've created a possible point of contention on which approach should be used in which situation. This wastes time and effort.

@ftxqxd
Copy link
Contributor

ftxqxd commented Jul 8, 2014

@Valloric I agree—in addition to your points, removing the existing syntax would make HKT a lot nicer. Right now the colon is inconsistent—in function arguments, it denotes a type, but in type parameters, it denotes trait bounds. The colon is already used to denote the type of a value, so inside type parameters they could also be used to denote the kind of a type:

fn f<A: type<*> -> *>(x: A<int>) -> A<uint> { ... } // bikeshed

1i             // sample value
fn(int) -> int // sample type (function)
type<*> -> *   // sample kind (type function)

@dobkeratops
Copy link

you could say the colon is the type of a type, or a bound. (value bounded as type, type bounded as trait) .. its consistent enough

@rkjnsn
Copy link
Contributor

rkjnsn commented Aug 5, 2014

At first glance, I really like the ideas put forward by @glaebhoerl in this comment.

@bluss
Copy link
Member

bluss commented Aug 5, 2014

Great proposal, at least in identifying a problem.

The bounds syntax is very noisy, and in its position it obscures what the line of code really does at a first glance:

impl<K:Hash+Eq,V> HashMap<K, V>

What you want to read easily is: What is the implementing type. Is it implementing a trait and if so, which? The bounds are secondary to this.

This is true both in source files as well as in documentation; The docs use the same syntax to display the trait information, see for example the list of trait implementations here

Would it be possible to reduce the noise even more, to something like this:

impl HashMap<K, V>
    where K: Hash + Eq, V
{}

impl Clone for HashMap<K, V>
    where K: Clone, V: Clone
{}

It is much easier to read.

@lilyball
Copy link
Contributor

lilyball commented Aug 5, 2014

@bluss That doesn't work because K and V have not already been introduced as type parameters yet. What if V is the name of an existing type?

…idispatch pattern, as it now seems that multidispatch is frequent enough to merit better support.
@nikomatsakis
Copy link
Contributor Author

@brson -- I think this is ready to merge.

@nikomatsakis
Copy link
Contributor Author

ping @brson

@brson brson merged commit 5bc42e8 into rust-lang:master Sep 30, 2014
@brson
Copy link
Contributor

brson commented Sep 30, 2014

Merged as RFC 66. Tracking. Discussion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-constraints Proposals relating to constraints & bounds. 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.