Operator traits with template type parameters


#1

I’m starting to understand what Rust’s coherence restrictions are, but it really seems like they aren’t quite right. I’ve read “Little Orphan Impls”, and it seems like there has to be a better way which just hasn’t been considered yet.

As an example, consider a template Matrix type from one crate and Decimal type from another crate. The Matrix should support scalar multiplication on both the left and the right

let d: Decimal = value();
let m: Matrix<decimal> = ident();

let md = m * d;
let dm = d * m;

In the crate for Matrix, you can have:

impl<T: Mul<T, Output=T>>    Mul<T> for Matrix<T>    { ... }

But the symmetric implementation is not allowed:

impl<T: Mul<T, Output=T>>    Mul<Matrix<T>> for T    { ... }

This means you can’t generically specify scalar multiplication from the left, and anyone who intends to use Rust for math or science should be a bit annoyed about that:

let md = m * d;  // this one works just like you'd expect
let dm = d * m;  // this one doesn't but really should

It turns out you can implement the trait if you don’t use a type parameter:

impl Mul<Decimal> for Matrix { ... }
impl Mul<Matrix> for Decimal { ... }

And that naturally leads to a workaround using a macro to explicitly implement all the operators necessary to marry Decimal and Matrix. Something like:

implement_matrix_operators!(Decimal)

However, it seems like requiring macros to implement templates means that type parameters aren’t doing what they should be. This same issue is going to come up in Complex numbers, linear algebra Vector, MultiArrays (tensors), Quaternions, and so on…

Is there any chance of fixing this in some future version of Rust?


#2

I’d like to point out that other languages and libraries have solved similar problems before. For instance the Common Lisp Object System faces a similar problem when resolving which multimethod to dispatch to. And I’m hesitant to praise C++, but templates in C++ have some notion of resolving to the most specific signature.


#3

More flexible coherence rules are one of the high priority items for post-1.0 improvements (see here, under “Specialization”). And here’s an open issue about it.

You also may find the most recent orphan rules RFC to be a good read: RFC 1023


#4

Thanks for the links, but I didn’t see any specific discussion regarding traits for binary operators so that left and right implementations are allowed with type parameters. It’s possible negative bounds is supposed to address this somehow, and maybe I misunderstood. Please excuse my cynicism, but it looks to me like the plan is to add even more complexity before getting the simple stuff right.

Looking at the Orphan Rules thread, JRoush’s suggestion looks very elegant:

impl Add for (u32, MyInt) { ... } // legal
impl Add for (MyInt, u32) { ... } // legal
impl<T> Add for (MyNum<T>, u32) { ... } // legal
impl<T> Add for (T, u32) { ... } // error: no local type in Self
impl<T> Add for (MyInt, T) { ... } // error: uncovered "T" in Self

It’s also attractive that the types show up in the correct order. To which I would specifically add:

impl<T> Add for (MyNum<T>, T) { ... } // legal: T is covered
impl<T> Add for (T, MyNum<T>) { ... } // also legal

Can crates be mutually dependent? If so, then there is still the problem with two modules trying to implement common operators:

impl<T> Add for (HisNum<T>, HerNum<T>) { ... } // allowed in one 
impl<T> Add for (HerNum<T>, HisNum<T>) { ... } // but not both

Perhaps at that point, a multiply defined error is acceptable because they’re both clearly relying on each other.