Nathan Lafferty
Nathan Lafferty

Reputation: 2098

How to implement bidirectional LHS and RHS operators in Rust?

In Rust you can overload operators (+, -, /, *, +=, etc.). I have a simple add implementation for my Vec3 type:

use std::ops::Add;

struct Vec3 {
    e0: f32,
    e1: f32,
    e2: f32,
}

impl Add<f32> for &Vec3 {
    type Output = Vec3;

    fn add(self, other: f32) -> Vec3 {
        Vec3 {
            e0: self.e0 + other,
            e1: self.e1 + other,
            e2: self.e2 + other,
        }
    }
}

And I can use it by doing:

let result = my_vec_3 + 43f32;

But doing it the other way errors out:

let this_wont_compile = 43f32 + my_vec_3;
error[E0277]: cannot add `Vec3` to `f32`
  --> src/lib.rs:23:35
   |
23 |     let this_wont_compile = 43f32 + my_vec_3;
   |                                   ^ no implementation for `f32 + Vec3`
   |
   = help: the trait `std::ops::Add<Vec3>` is not implemented for `f32`

I know I could write an implementation for impl Add<&Vec3> for f32 but that's what I'm looking to automate.

How can you write your implementation such that the LHS and RHS are interchangeable? Is it possible in Rust?

Upvotes: 3

Views: 1469

Answers (1)

Nathan Lafferty
Nathan Lafferty

Reputation: 2098

The language does not automate this for you.

Your options are:

  1. Write the implementations yourself
  2. Build a macro and use metaprogramming

zrzka was kind enough to construct a Rust Playground that provides example macros for this specific use-case.

This question also provides some helpful hints since the Rust Source Code itself uses these macros to automate some of the tedium.

One concern I had was if I used a macro I would have to call it inline (like vec![]). Since macros are expanded at compile time, your macro will generate your functions for you that you can call normally. RLS will still provide syntax support and everything will work as you expect it should.

Here's the implementation I ended up going with. I'm sure much more could be automated (forward_ref_binop for one) but I'm happy with it.

/// Generates the operations for vector methods. `let result = my_vec_3 + my_other_vec3`
/// Handles `Vec3, Vec3`, `Vec3, &Vec3`, `&Vec3, Vec3`, `&Vec3, &Vec3`
/// `vec3_vec3_op(ops::AddAssign, add_assign)` (note the camelcase add_assign name)
macro_rules! vec3_vec3_op {
    ($($path:ident)::+, $fn:ident) => {
        impl $($path)::+<Vec3> for Vec3 {
            type Output = Vec3;

            fn $fn(self, other: Vec3) -> Self::Output {
                Vec3 {
                    e0: self.e0.$fn(other.e0),
                    e1: self.e1.$fn(other.e1),
                    e2: self.e2.$fn(other.e2),
                }
            }
        }

        impl $($path)::+<&Vec3> for &Vec3 {
            type Output = Vec3;

            fn $fn(self, other: &Vec3) -> Self::Output {
                Vec3 {
                    e0: self.e0.$fn(other.e0),
                    e1: self.e1.$fn(other.e1),
                    e2: self.e2.$fn(other.e2),
                }
            }
        }

        impl $($path)::+<&Vec3> for Vec3 {
            type Output = Vec3;

            fn $fn(self, other: &Vec3) -> Self::Output {
                Vec3 {
                    e0: self.e0.$fn(other.e0),
                    e1: self.e1.$fn(other.e1),
                    e2: self.e2.$fn(other.e2),
                }
            }
        }

        impl $($path)::+<Vec3> for &Vec3 {
            type Output = Vec3;

            fn $fn(self, other: Vec3) -> Self::Output {
                Vec3 {
                    e0: self.e0.$fn(other.e0),
                    e1: self.e1.$fn(other.e1),
                    e2: self.e2.$fn(other.e2),
                }
            }
        }
    };
}

/// Generates the operations for vector method assignment. `my_vec += my_other_vec`
/// Handles `Vec3, Vec3` and `Vec3, &Vec3`
/// `vec3_vec3_opassign(ops::AddAssign, add_assign)` (note the camelcase add_assign name)
macro_rules! vec3_vec3_opassign {
    ($($path:ident)::+, $fn:ident) => {
        impl $($path)::+<Vec3> for Vec3 {
            fn $fn(&mut self, other: Vec3) {
                self.e0.$fn(other.e0);
                self.e1.$fn(other.e1);
                self.e2.$fn(other.e2);
            }
        }

        impl $($path)::+<&Vec3> for Vec3 {
            fn $fn(&mut self, other: &Vec3) {
                self.e0.$fn(other.e0);
                self.e1.$fn(other.e1);
                self.e2.$fn(other.e2);
            }
        }
    };
}

/// Generates the operations for method assignment. `my_vec += f32`
/// `vec3_opassign(ops:AddAssign, add_assign)` (note the camelcase add_assign name)
macro_rules! vec3_opassign {
    ($($path:ident)::+, $fn:ident, $ty:ty) => {
        impl $($path)::+<$ty> for Vec3 {
            fn $fn(&mut self, other: $ty) {
                self.e0.$fn(other);
                self.e1.$fn(other);
                self.e2.$fn(other);
            }
        }
    }
}

/// Generates the operations for the method. `let result = my_vec + 4f32`
/// Handles `Vec3, T`, `T, Vec3`, `&Vec3, T`, `T, &Vec3`
/// `vec3_op!(ops:Add, add, f32)`
macro_rules! vec3_op {
    ($($path:ident)::+, $fn:ident, $ty:ty) => {
        // impl ops::Add::add for Vec3
        impl $($path)::+<$ty> for Vec3 {
            type Output = Vec3;

            // fn add(self, other: f32) -> Self::Output
            fn $fn(self, other: $ty) -> Self::Output {
                Vec3 {
                    // e0: self.e0.add(other)
                    e0: self.e0.$fn(other),
                    e1: self.e1.$fn(other),
                    e2: self.e2.$fn(other),
                }
            }
        }

        impl $($path)::+<$ty> for &Vec3 {
            type Output = Vec3;

            fn $fn(self, other: $ty) -> Self::Output {
                Vec3 {
                    e0: self.e0.$fn(other),
                    e1: self.e1.$fn(other),
                    e2: self.e2.$fn(other),
                }
            }
        }

        impl $($path)::+<Vec3> for $ty {
            type Output = Vec3;

            fn $fn(self, other: Vec3) -> Self::Output {
                Vec3 {
                    e0: self.$fn(other.e0),
                    e1: self.$fn(other.e1),
                    e2: self.$fn(other.e2),
                }
            }
        }

        impl $($path)::+<&Vec3> for $ty {
            type Output = Vec3;

            fn $fn(self, other: &Vec3) -> Self::Output {
                Vec3 {
                    e0: self.$fn(other.e0),
                    e1: self.$fn(other.e1),
                    e2: self.$fn(other.e2),
                }
            }
        }
    }
}

macro_rules! vec3_op_for {
    ($ty: ty) => {
        vec3_op!(ops::Add, add, $ty);
        vec3_op!(ops::Sub, sub, $ty);
        vec3_op!(ops::Mul, mul, $ty);
        vec3_op!(ops::Div, div, $ty);
        vec3_opassign!(ops::AddAssign, add_assign, $ty);
        vec3_opassign!(ops::SubAssign, sub_assign, $ty);
        vec3_opassign!(ops::MulAssign, mul_assign, $ty);
        vec3_opassign!(ops::DivAssign, div_assign, $ty);
    };
}

vec3_vec3_op!(ops::Add, add);
vec3_vec3_op!(ops::Sub, sub);
vec3_vec3_op!(ops::Mul, mul);
vec3_vec3_op!(ops::Div, div);
vec3_vec3_opassign!(ops::AddAssign, add_assign);
vec3_vec3_opassign!(ops::SubAssign, sub_assign);
vec3_vec3_opassign!(ops::MulAssign, mul_assign);
vec3_vec3_opassign!(ops::DivAssign, div_assign);
vec3_op_for!(f32);

From here if I expanded my Vec3 class to handle generics, it would be trivial to add additional types.

Upvotes: 4

Related Questions