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 > Am 22.07.2014 18:50, schrieb Allen Welkie: > >* 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 > believe) was to use tuples for that: > > 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 > should work well together with generic code: > > 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 > >
_______________________________________________ Rust-dev mailing list Rust-dev@mozilla.org https://mail.mozilla.org/listinfo/rust-dev