Proliferation of structs to implement trait with various bounds, another way?


#1

I have created a trait that is basically meant to wrap a type that I can get in a send/sync way.

pub trait ParamBindingGet<T>: Send + Sync {
    fn get(&self) -> T;
}

and I’m creating a bunch of implementations of this for transformations/combinations… for instance, adding or multiplying 2 values.

/// Sum two numeric bindings.
pub struct GetSum<T, L, R> {
    left: Arc<L>,
    right: Arc<R>,
    _phantom: PhantomData<fn() -> T>,
}

/// Multiply two numeric bindings.
pub struct GetMul<T, L, R> {
    left: Arc<L>,
    right: Arc<R>,
    _phantom: PhantomData<fn() -> T>,
}

impl<T, L, R> GetSum<T, L, R>
where
    T: Send,
    L: ParamBindingGet<T>,
    R: ParamBindingGet<T>,
{
    /// Construct a new `GetSum`
    ///
    /// # Arguments
    ///
    /// * `left` - the binding for left value of the sum
    /// * `right` - the binding for the right value of the sum
    pub fn new(left: Arc<L>, right: Arc<R>) -> Self {
        Self {
            left,
            right,
            _phantom: Default::default(),
        }
    }
}

impl<T, L, R> ParamBindingGet<T> for GetSum<T, L, R>
where
    T: std::ops::Add + num::Num,
    L: ParamBindingGet<T>,
    R: ParamBindingGet<T>,
{
    fn get(&self) -> T {
        self.left.get().add(self.right.get())
    }
}

impl<T, L, R> GetMul<T, L, R>
where
    T: Send,
    L: ParamBindingGet<T>,
    R: ParamBindingGet<T>,
{
    /// Construct a new `GetMul`
    ///
    /// # Arguments
    ///
    /// * `left` - the binding for left value of the multiplication
    /// * `right` - the binding for the right value of the multiplication
    pub fn new(left: Arc<L>, right: Arc<R>) -> Self {
        Self {
            left,
            right,
            _phantom: Default::default(),
        }
    }
}

impl<T, L, R> ParamBindingGet<T> for GetMul<T, L, R>
where
    T: std::ops::Mul + num::Num,
    L: ParamBindingGet<T>,
    R: ParamBindingGet<T>,
{
    fn get(&self) -> T {
        self.left.get().mul(self.right.get())
    }
}

It is nice that I can simply implement the trait for only the bounds that are important for say, Add or Mul, but really these are just binary operators and could be combined if I just had the type specify an operation and then further constrain the bounds to include Add + Mul etc.

something like:

pub enum BinaryMathOp<L, R, T> {
  Add { left: L, right: R },
  Mul { left: L, right: R}
}

impl<T, L, R> ParamBindingGet<T> for BinaryMathOp<T, L, R>
where
    T: num::Num + std::ops::Add + std::ops::Mul,
    L: ParamBindingGet<T>,
    R: ParamBindingGet<T>,
{
    fn get(&self) -> T {
        //maybe not quite right, but you get the idea
        match self {
            &Add { left, right } => left.get().add(right.get())
            &Mul { left, right } => left.get().mul(right.get())
        }
    }
}

So, maybe I’m over-engineering things and this is just fine but I wonder if there is a better way? Is there a way that I can implement the trait I care about for each operation, with only the bounds that that operation needs? With T: Add just for the bounds that Add needs? and do the same for Mul, Div (with some additional divide by zero checking) etc without having to have an individual struct per operation?

Also, in a side note, is there some nice way to specify the phantom in this case so that all of the operators are initialized in the same way? Add { left, right}, Mul { left, right }