Discussion:
[rust-dev] Conflicting implementations of a trait
Lionel Parreaux
2014-08-03 23:27:49 UTC
Permalink
Hi,

I just stumbled upon your message, and that reminded me of something I had
thought about.
In Scala, type classes are interfaces, and type class instances are types
implementing these interfaces.
But the "receiver type" (the self type, the type of the instance), only
serves to define operations, not store any data. It will eventually store
the vtable for these operations though, when passed to methods or objects
requiring the associated type class (type classes are a runtime mechanism).
Type class Add in Scala would be something symmetric like:
trait Add[A,B,C] { def add (a:A, b:B): C }

Now, I was thinking that this was also a very nice way of doing things in
the context of a static system.
"Add" may not be a very compelling example for this, so let's take the
example of a "Hash" trait.

If we defined Hash the Scala way in Rust (with a separate self type: trait
Hash<T>), it would mean we could create as many versions of it as we'd
like, without coherence or safety problems, because we'd be using a
different self-type each time.
There are two important use cases to distinguish:
- A HashSet type will be parametrized not only with the element type T, but
also with the hashing implementation for this type, HT:Hash<T>. This is
required to ensure we only ever use one hash function for all operations of
this set.
- If a particular function wants to be able to get the hash of type X, it
will require a type parameter HT:Hash<X>. Such a function could locally use
a hash table with the provided hash function, for example. There is no need
that the type X be bound to a single hashing function.
This way, Hashing is a concept totally decoupled the types it applies on,
but we retain necessary coherence and link-safety.

There seem to be many traits that would benefit from this kind of
decoupling. For example, it could be useful to define several different
orderings for a single type. A particular case: we may want to only locally
define some ordering for a type which has no semantic notion of ordering,
just so that we can store this type in a TreeSet.
Another one : Show, to get a string representation of an object, or
Iterator (there may be different ways of iterating on a string; eg: by word
or by char?).
I think some traits are inherently, semantically coupled to a type (Clone,
Add?). For the others it may be useful to have them decoupled.


As for the problem of argument representation (references or value), have
you thought about some trait of the form "ArgRepr<T>", that has a static
"wrap" constructor producing the appropriate representation?
With each type definition X, we would implement ArgRepr<X> for the
appropriate argument representation of this type. Then, we'd have:
fn foo<T, AT:ArgRepr<T>, U, AU:ArgRepr<U>, O> (x: T, y: U) -> O
where (AT,AU): Mul<O>
{
AT::wrap(&x) * AU::wrap(&y) // calls to wrap hopefully inlined,
otherwise the whole thing is pretty useless
}

Well, that does look quite ugly and verbose :/
Maybe some help from the syntax/language would make it better.

Cheers,
LP.


Sebastian Gesemann s.gesemann at gmail.com
Tue Jul 22 10:34:42 PDT 2014
* Can there be two simultaneous implementations of a generic trait? I ask
*>* because I want to extend the Complex class to allow for multiplication by
*>* scalars, so that you can use "a * b" where "a" and "b" can be either
*>* scalars or Complex.
*
[snip]
Something like this was my first attempt in Rust. I was able to define
two own types (complex and imaginary) which I could mix with f64 for
multiplication, addition, etc.
But it required a kind of "double dispatch". Niko explained it here:http://smallcultfollowing.com/babysteps/blog/2012/10/04/refining-traits-slash-impls/
Unfortunately, given how these traits are defined now, design requires a
bit of foresight. If you want to mix types like this for binary
operations eventually, you should probably start this kind of
dispatching early on. You can't do that with num's complex struct now.
Its Add/Mul/etc impls weren't designed with double-dispatch in mind. For
now, you would have to define your own types like I did.
But there is a chance that the binary operator traits change. For a
binary operator like + and * there is no clear "receiver" (an object you
call an add function on). IMHO the operands should be treated equally.
One approach that I saw mentioned by Niko (in another blog post I
trait Add<Out> {
fn add(self) -> Out;
}
impl Add<Complex<f64>> for (f64,Compex<f64>) {
fn add((lhs, rhs) : (f64, Complex<f64>)) -> Complex<f64> {
...
}
}
And this makes it much easier to extend the interface of certain types
together.
On the other hand, there still needs to go some thought into this with
respect to passing operands by value or reference. You don't want
unnecessary clones. And you probably don't want operands to be
moved-from in some cases. And the way these kinds of traits are refined
fn foo<T,U,O>(x: T, y: U) -> O
where ???: Mul<O>
{
x * y
}
Ideally, this should work for every type T and U that can be multiplied
somehow. The question however is, how to write down the type bound?
Should we write
(&T,&U): Add<O>
to avoid moving? Should we write
(T,U): Add<O>
for a nicer, more intuitive syntax perhaps? I don't know. If you have a
good idea how to do that, I'm all ears. I'm very much interested in
getting easily overloadable operators without the pain of
double-dispatch and without the pain of clumsly type bounds for generic
functions that only work for half the cases due to references and such.
Cheers!
sg
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/rust-dev/attachments/20140804/a5aa1e79/attachment.html>
Loading...