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 upWhere clauses for more expressive bounds #135
Conversation
This comment has been minimized.
This comment has been minimized.
thehydroimpulse
commented
Jun 24, 2014
|
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 Are associated types going to have a separate RFC? |
This comment has been minimized.
This comment has been minimized.
bill-myers
commented
Jun 24, 2014
|
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:
For the formatting issue one could write it ilke this
Which also allows to reuse types for multiple declarations, like this (this has been suggested in the past):
Instead of "template", one could use "where" or perhaps "type", or even bare angle brackets with no keyword at all, like this:
If nesting is allowed, then there is a slight ambiguity, since something like this:
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. |
This comment has been minimized.
This comment has been minimized.
|
@bill-myers that was #122 |
This comment has been minimized.
This comment has been minimized.
|
|
lilyball
reviewed
Jun 24, 2014
| 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()`: |
This comment has been minimized.
This comment has been minimized.
lilyball
Jun 24, 2014
Contributor
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?
This comment has been minimized.
This comment has been minimized.
|
On Tue, Jun 24, 2014 at 09:19:39AM -0700, bill-myers wrote:
It is to some extent, yes. I suppose the only thing it rules out are
Speaking personally, I've always found the C++ style confusing because |
This comment has been minimized.
This comment has been minimized.
ben0x539
commented
Jun 24, 2014
|
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 |
This comment has been minimized.
This comment has been minimized.
|
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). |
nrc
reviewed
Jun 24, 2014
| fn reduce<T:Clone>(xs: &[T]) -> T | ||
| where () : Add<T,T,T> | ||
| // ^~~~~~~~~~~~~~~ Note: DRY | ||
| { |
This comment has been minimized.
This comment has been minimized.
nrc
Jun 24, 2014
Member
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.
This comment has been minimized.
This comment has been minimized.
lilyball
Jun 24, 2014
Contributor
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.
This comment has been minimized.
This comment has been minimized.
lilyball
Jun 24, 2014
Contributor
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?
This comment has been minimized.
This comment has been minimized.
ben0x539
Jun 24, 2014
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.
This comment has been minimized.
This comment has been minimized.
lilyball
Jun 24, 2014
Contributor
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.
This comment has been minimized.
This comment has been minimized.
lilyball
Jun 25, 2014
Contributor
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.
This comment has been minimized.
This comment has been minimized.
ben0x539
Jun 25, 2014
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. ;)
This comment has been minimized.
This comment has been minimized.
lilyball
Jun 25, 2014
Contributor
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)
{ ... }
This comment has been minimized.
This comment has been minimized.
nikomatsakis
Jun 28, 2014
Author
Contributor
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
Addimplementations on the single type(). Isn't that a violation of the same coherence rule that preventedAdd<Complex, Complex>+Add<int, Complex>onComplex?
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.)
This comment has been minimized.
This comment has been minimized.
nikomatsakis
Jun 28, 2014
Author
Contributor
On Tue, Jun 24, 2014 at 03:48:51PM -0700, Kevin Ballard wrote:
Actually even this is unnecessarily repetitious. The
Addimpl 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.
This comment has been minimized.
This comment has been minimized.
|
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 And if we do decide to keep both, at what threshold of complexity is one supposed to stop using |
This comment has been minimized.
This comment has been minimized.
|
@bstrie With a 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 |
This comment has been minimized.
This comment has been minimized.
|
To remedy this I’d probably make all type declarations implicit, which solves the issue of separating type variable declarations from type parameters: 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 This means that there are no hacky workarounds using (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 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);
}
} |
sellibitze
reviewed
Jun 25, 2014
| } | ||
| } | ||
|
|
||
| Now the expression `a + b` would be sugar for `Add::add((a, b))`. |
This comment has been minimized.
This comment has been minimized.
sellibitze
Jun 25, 2014
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.
This comment has been minimized.
This comment has been minimized.
lilyball
Jun 25, 2014
Contributor
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))
This comment has been minimized.
This comment has been minimized.
@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 |
This comment has been minimized.
This comment has been minimized.
|
@bstrie The RFC does already demonstrate that constraints inside the func foo<T: Clone>(x: T) -> T;
func foo<T>(x: T) -> T where T: Clone;
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. |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
dobkeratops
commented
Jun 27, 2014
|
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; 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 It's a shame because this is orthogonal to Rusts' core pillars. 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. 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; 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. |
This comment has been minimized.
This comment has been minimized.
|
On Tue, Jun 24, 2014 at 03:42:19PM -0700, Ben Striegel wrote:
Since as how the old bounds are syntactic sugar for
There is just one set of constraints and they are unioned together, so there is no problem here.
(There are other equivalent ways one could write the declaration as well.)
Yeah, I don't know.
Speaking personally, it'd probably be "any bound more complicated than |
This comment has been minimized.
This comment has been minimized.
|
On Fri, Jun 27, 2014 at 05:01:35AM -0700, dobkeratops wrote:
I don't know precisely what you mean, to be honest. I think you mean |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
UtherII
commented
Jul 1, 2014
|
I like the syntax proposed by gashe. |
gasche
referenced this pull request
Jul 1, 2014
Closed
Syntax sugar for prefix-style type parameter lists. #122
This comment has been minimized.
This comment has been minimized.
|
As far as the actual feature being proposed is concerned, which is 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.:
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 The
with no funny business required. |
This comment has been minimized.
This comment has been minimized.
|
I first encountered the idea of using a tuple for multiple dispatch from PyPy. Here is a link to PyPy's pairtype. |
This comment has been minimized.
This comment has been minimized.
ghost
commented
Jul 2, 2014
|
The following example of yours is unreadable mainly because of the code formatting style you use:
After editing only whitespaces:
+4 lines, but the readability easily wins the trade-off IMO. |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
|
One more thing:
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.) |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
|
@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.) |
This comment has been minimized.
This comment has been minimized.
|
@kballard Semantically speaking, if the user writes an unsatisfiable constraint, that means they've written an uncallable function. Which is not the same thing as an illegal function. Seems like a warning might be more appropriate. @nikomatsakis I had a slightly more provocative comment higher up. (I'm not requesting a response, it's only that with GitHub not having |
This comment has been minimized.
This comment has been minimized.
|
@glaebhoerl I'm ok with a warning if it's unsatisfiable, all I really care about is that the function implementation is flagged as problematic even if it's never called. But I suspect an error will be easier to deal with, because it avoids the problems of trying to pretend the bound exists on that concrete type when type-checking the function body. |
This comment has been minimized.
This comment has been minimized.
|
@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. 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. |
This comment has been minimized.
This comment has been minimized.
|
@Ericson2314 I was thinking kind signatures would have a prefix form-follows-declaration and vaguely C++-like syntax, e.g. |
This comment has been minimized.
This comment has been minimized.
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:
|
This comment has been minimized.
This comment has been minimized.
|
@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) |
This comment has been minimized.
This comment has been minimized.
dobkeratops
commented
Jul 8, 2014
|
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 |
This was referenced Jul 17, 2014
This comment has been minimized.
This comment has been minimized.
|
At first glance, I really like the ideas put forward by @glaebhoerl in this comment. |
This comment has been minimized.
This comment has been minimized.
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:
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:
It is much easier to read. |
This comment has been minimized.
This comment has been minimized.
|
@bluss That doesn't work because |
aturon
referenced this pull request
Aug 13, 2014
Merged
RFC: associated items and multidispatch #195
This comment has been minimized.
This comment has been minimized.
|
@brson -- I think this is ready to merge. |
nrc
assigned
nikomatsakis
Sep 4, 2014
alexcrichton
force-pushed the
rust-lang:master
branch
from
6357402
to
e0acdf4
Sep 11, 2014
aturon
force-pushed the
rust-lang:master
branch
from
4c0bebf
to
b1d1bfd
Sep 16, 2014
This comment has been minimized.
This comment has been minimized.
|
ping @brson |
brson
merged commit 5bc42e8
into
rust-lang:master
Sep 30, 2014
This comment has been minimized.
This comment has been minimized.
|
Merged as RFC 66. Tracking. Discussion |
nikomatsakis commentedJun 24, 2014
•
edited by mbrubeck
Add
whereclauses, which provide a more expressive means of specifying trait parameter bounds. Awhereclause 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:
Option<T>whereTis a type parameter.Rendered view.