4

I have a simple struct that should implement basic math operators. Initially I didn't want these to consume the operands, so I implemented the traits for references, for example for Add:

impl<'a, 'b> Add<&'b MyType> for &'a MyType {
    type Output = MyType;

    fn add(self, rhs: &'b MyType) -> Self::Output {
        // Implementation...
    }
}

This allows me to do:

let result = &v1 + &v2;

Where v1 and v2 are of type MyType.

Then I realized that sometimes it is syntactically more convenient to consume the operands, for example when doing:

let result = &v1 + &v2 + &v3;

Because there is an intermediate result the above won't compile and you have to do:

let result = &v1 + &(&v2 + &v3);

So I ended up implementing the other permutations of move and borrow, which just defer to the first one:

impl<'a> Add<&'a MyType> for MyType {
    type Output = MyType;

    fn add(self, rhs: &'a MyType) -> Self::Output {
        &self + rhs
    }
}

impl<'a> Add<MyType> for &'a MyType {
    type Output = MyType;

    fn add(self, rhs: MyType) -> Self::Output {
        self + &rhs
    }
}

impl Add<MyType> for MyType {
    type Output = MyType;

    fn add(self, rhs: MyType) -> Self::Output {
        &self + &rhs
    }
}

This works, but is cumbersome.

I looked for an easier way, such as using Borrow<T>:

impl<B> Add<B> for B
where
    B: Borrow<MyType>,
{
    type Output = MyType;

    fn add(self, rhs: B) -> Self::Output {
        // Implementation...
    }
}

But that won't compile, understandably, due to type parameter B must be used as the type parameter for some local type.

Are there any other tricks to avoid having all these boilerplate implementations for Add/Sub/Mul/Div, etc?

Update:

@EvilTak made a suggestion in the comments which cuts down on the boilerplate combinations by implementing the B: Borrow<MyType> version on MyType and &MyType explicitly. This is a good improvement:

impl<'a, B> Add<B> for &'a MyType
where
    B: Borrow<MyType>,
{
    type Output = MyType;

    fn add(self, rhs: B) -> Self::Output {
        // Implementation...
    }
}

impl<B> Add<B> for MyType
where
    B: Borrow<MyType>,
{
    type Output = MyType;

    fn add(self, rhs: B) -> Self::Output {
        &self + rhs
    }
}
James Thurley
  • 2,650
  • 26
  • 38
  • 4
    A macro perhaps? – Chayim Friedman Nov 08 '22 at 16:01
  • I think that will be what I'll have to do if I can't find a neater way. – James Thurley Nov 08 '22 at 16:23
  • 2
    You could manually impl `Add>` for `MyType` and `&MyType` (and any other reference-like types you might want to implement it on). It won't cut down on _all_ the boilerplate, but it will let you write only `n` impls instead of `n^2` impls for all `MyType` owned/reference type combinations. – EvilTak Nov 08 '22 at 17:55
  • @EvilTak, yes, that's a good step in the right direction, thank you. – James Thurley Nov 09 '22 at 09:58
  • An other solution, since you take references, would be to output a reference too (that is, something like `type Output = &'a MyType;`. This is not always possible, but if it is in your case it might cut down a lot of boilerplate. – jthulhu Nov 09 '22 at 10:00

1 Answers1

1

Having gone down this rabbit hole, I'll answer my own question.

I started off using Borrow to reduce the number of functions I would need to implement:

impl<'a, B> Add<B> for &'a MyType
where
    B: Borrow<MyType>,
{
    type Output = MyType;

    fn add(self, rhs: B) -> Self::Output {
        // Implementation...
    }
}

impl<B> Add<B> for MyType
where
    B: Borrow<MyType>,
{
    type Output = MyType;

    fn add(self, rhs: B) -> Self::Output {
        &self + rhs
    }
}

This worked well until I also needed to add MyType and MyOtherType together.

Attempting to implement that using Borrow<T> gave the error conflicting implementations of trait:

impl<'a, B> Mul<B> for &'a MyType
where
    B: Borrow<MyOtherType>,
{
    type Output = MyOtherType;

    fn mul(self, rhs: B) -> Self::Output {
        /// Implementation...
    }
}

This is because a type could theoretically implement both Borrow<MyType> and Borrow<MyOtherType> at the same time, and the compiler wouldn't know which implementation to use.

At that point I decided to try the macro route instead. As you would expect, this has been done before by others.

A couple of different places suggested using impl_ops, which has since been replaced by auto_ops.

This crate lets you define all the various combinations for an operator by doing something like:

impl_op_ex!(+ |a: &DonkeyKong, b: &DonkeyKong| -> DonkeyKong { DonkeyKong::new(a.bananas + b.bananas) });

However, this crate has the limitation of not working with generics. In my case MyType is actually Matrix<const M: usize, const N: usize>, so I needed generics support.

I then came across auto_impl_ops, which lets you generate all the various add combinations from a single AddAsign trait implementation (and same for other ops), and also supports generics.

use std::ops::*;
# 
# #[derive(Clone, Default)]
# struct A<T>(T);

#[auto_impl_ops::auto_ops]
impl<M> AddAssign<&A<M>> for A<M>
where
    for<'x> &'x M: Add<Output = M>,
{
    fn add_assign(&mut self, other: &Self) {
        self.0 = &self.0 + &other.0;
    }
}

One limitation here is that the result always has to be the same type as Self, which may not be the case if you are doing, for example, a matrix multiplied by a vector.

Another possible issue is that for a binary operator the crate will clone the left-hand value before using your assignment operator implementation, returning the clone. For matrix multiplication I also had to then clone self in the MulAssign implementation, or I would be overwriting the data I'm still using for the matrix multiplication. This means there is at least one redundant memory copy here, which I wouldn't have if I implemented the operator manually.

I've gone with this library for now. I'll try and update here if that changes.

James Thurley
  • 2,650
  • 26
  • 38