Overloading operator on generic type with trait bounds

Hi, I'm kinda new to Rust but I have background in other languages. On a personal project, I needed to overload operators on a generic type with trait bounds.The code below is a simplified version of code I ended up with after reading some docs, looking at examples and following compiler suggestions:

struct Num <V:, G>
where
    V : ops::Neg + ops::Neg<Output = V> + ops::Add + ops::Add<Output = V>,
    G: PartialEq
{
    val: V,
    group: G
}

impl<V,G> ops::Add for Num<V, G>
where
    V : ops::Neg + ops::Neg<Output = V> + ops::Add + ops::Add<Output = V>,
    G : PartialEq
{
    type Output = Option<Num<V, G>>;

    fn add(self, other: Self) -> Self::Output {
        if self.group == other.group {
            let sum = self.val + other.val;
            Some(Num { val: sum, group : self.group })
        } else {
            None
        }
    }
}

impl<V,G> ops::Neg for Num<V, G>
where
    V : ops::Neg + ops::Neg<Output = V> + ops::Add + ops::Add<Output = V>,
    G : PartialEq
{
    type Output = Num<V, G>;

    fn neg(self) -> Num<V, G> {
        Num { val: -self.val, group: self.group }
    }
}


Short summary:

The code above defines a special type Num which has a value and a group. The values are added normally to create a new Num, provided the groups are equal, and the 'sum' Num simply inherits the common group. Adding two Nums from different groups is invalid, and therefore should return None instead.

The negation operation only negates the value and inherits the group.


The code seems to work. However, I have a couple of questions:

  1. I noticed that there is a lot of repetition for the where clause (in the struct, and all of the overload implementations). Is there someway to make this better?
  2. The compiler suggested adding ops::Neg<Output = V> and ops::Add<Output = V>.
    2.a I can't seem to find what the <Output = V> is called. It's not in the std::ops doc and other operator overloading examples on the internet.
    2.b Is the Output in <Output = V> related to the one in type Output = Num<V, G>;

One way to cut down on the number and length of bounds is to only state them where necessary. I.e. define the struct without any trait bounds, and write the trait impls with only those bounds that are actually required for each implementation.

You should read V: ops::Neg<Output = V> to mean
V: ops::Neg and V::Output == V

(the latter part with the == being pseudo-syntax for a constraint of two types being equal, and the ::Output comes from the Neg implementation, so more explicitly it would be <V as ops::Neg>::Output == V)

This means that the additional V: ops::Neg that you also have is redundant. Similarly for Add.

Also note that you could generalize the impl to allow a different output than input type, if you want to allow that. That could be done either by introducing another type argument to the impl, (impl<V,G,O>) and using that argument in the bound V: ops::Neg<Output = O>, and then type Output = Num<O, G>; would be a sensible definition; or the bound could be simplified to V: ops::Neg, you wouldn’t introduce another type argument, and you could define type Output = Num<V::Output, G>;. These two approaches I described in the previous sentence would be equivalent to each other. (In case my explanation is hard to follow, feel free to ask for code examples.)

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.