Trait bound on `type Output`


#1

I ran into an unexpected issue with generics while I was exploring. Say I have a generic Point struct:

struct Point<T> {
    x: T,
    y: T,
}

I want to define std::ops::Add for Point<T> as long as T itself implements Add:

impl<T: Add> Add for Point<T> { }

The problem is - I want the type Output here to also be Point<T>, but this doesn’t work:

impl<T: Add> Add for Point<T> {
    type Output = Point<T>;

    fn add(...) 
}

The reason is that even though you require T to implement Add, the Output of that Add doesn’t have to be T. I was forced to go with this workaround:

impl<T: Add> Add for Point<T> {
    type Output = Point<<T as Add>::Output>;

    fn add(self, rhs: Point<T>) -> Self::Output {
        Point {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }

}

So my question is, how do I restrict this impl block to only take T: Add where the Output of that add is also T? It seems like it should be a trait bound, but the problem is that it’s a restriction on type Output rather than on the generic type parameters.


#2

IIRC, you can say T: Add<Output=T>.


#3

Full example:

use std::ops::Add;

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Add for Point<T>
where
    T: Add<Output = T>,
{
    type Output = Point<T>;
    
    fn add(self, rhs: Point<T>) -> Self::Output {
        Point {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 10i32, y: 20i32 };
    let p2 = Point { x: 20i32, y: 30i32 };
    
    println!("{:?}", p1 + p2);
}

Another alternative which also works (and potentially lends somewhat more flexibility):

impl<T> Add for Point<T>
where
    T: Add,
    T::Output: Into<T>,
{
    type Output = Point<T>;
    
    fn add(self, rhs: Point<T>) -> Self::Output {
        Point {
            x: (self.x + rhs.x).into(),
            y: (self.y + rhs.y).into(),
        }
    }
}

#4

Worked like a charm. Thanks to both of you.


#5

Why isn’t that way better? It means, for example, that you can do Point<&f32> + Point<&f32> -> Point<f32>, which seems like a perfectly reasonable and good implementation to me.

Or you could even go all the way to

impl<T, U> Add<Point<U>> for Point<T> where T: Add<U> {
    type Output = Point<<T as Add<U>>::Output>;

    fn add(self, rhs: Point<U>) -> Self::Output {
        Point {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

#6

You’re right - “forced to go with this workaround” was a poor choice of words. It is more flexible.

Sometimes I worry about going too far with generics, though, making API functionality way more general than it needs to be. This more general form we’re talking about introduces what look like unexpected implicit conversions. I feel more comfortable if the compiler stops me and tells me to be explicit.


#7

It might be better, but changing Point<T> -> Point<U> as part of addition may ripple through uses of Point<T> where those uses expect to continue having Point<T> values, and not a different type.

Instead of Point<&f32> + Point<&f32> = Point<f32> case being important, it’s likely @axesilo may want Point<T> + &Point<T> = Point<T> and similar combinations.