Impl'ing Add to combine axes types

Say I have 3 different types of costs (e.g. wood, time, dollars). And I strong type them struct Wood(i64) etc. Now I want to be able to add them together to make a CombinedCosts(i16,i64,i32) struct. So I impl Add<Wood> for Time with Output = CombinedCosts, but I want the other way to work too so I impl Add<Time> for Wood, but I should be able to add in any order so I also need to do Wood+Dollars and Dollars+Wood and so on... and then because after one addition I will be working with a CombinedCosts instance it needs 6 Add impls to let it combine both ways with the individual types, and then also with itself so 7 total.

Now if there were say 10 types of cost... :sweat_smile:

In C++, CombinedCosts could be a tuple with axis keyed on type, and you could write a couple templates that would handle all the combinations. I'm less sure how to do this in Rust.

If I wanted to keep the code simple overall, I would not implement Add for the sub-component types, but only From.

impl Add<CombinedCosts> for CombinedCosts
impl From<Time> for CombinedCosts
impl From<Wood> for CombinedCosts
impl From<Dollars> for CombinedCosts

This will require you to write cost = cost + CombinedCosts::from(time) and so on, but it is simple. You could also provide a special method to make it slightly shorter to invoke these:

impl CombinedCosts {
    fn add_component(self, other: impl Into<CombinedCosts>) -> Self { ... }

If I wanted to provide the most general interface, I would use macros to generate all the desired impls.


I agree with everything above, and will just note that you can probably write

let cost: CombinedCosts = costs + wood.into();
let cost = CombinedCosts::from(time) + wood.into();

And if that's still too ambiguous, you could put methods on your sub-component types (via macro probably).

fn combined(self) -> CombinedCost { self.into() }
1 Like

I wrote this up in the playground, but may have gone a little overboard— The maintainability of this code is likely to be poor. If you impl_combined!{ Wood(i64), Time(i32), ... }, this will expand into struct declarations for Combined, Wood, Time, etc. with a bunch of trait implementations (where ... represents any of the individual types):

impl Copy for ...
impl Clone for ...
impl Debug for ...
impl Eq for ...
impl PartialEq for ...
impl PartialOrd for ...
impl Default for ...
impl Copy for Combined
impl Clone for Combined
impl Debug for Combined
impl Eq for Combined
impl PartialEq for Combined
impl PartialOrd for Combined
impl Default for Combined
impl std::ops::Add<...> for Combined
impl std::ops::Sub<...> for Combined 
impl std::ops::Add<Combined> for ...
impl std::ops::Sub<Combined> for ...
impl std::ops::Add<...> for ...
impl std::ops::Sub<...> for ...
impl std::ops::Add for ...
impl std::ops::AddAssign for ...
impl std::ops::Sub for ...
impl std::ops::SubAssign for ...
impl AsRef<...> for Combined
impl AsMut<...> for Combined
impl From<...> for Combined
impl std::ops::Add for Combined
impl<T> std::ops::AddAssign<T> for Combined where Self: std::ops::Add<T, Output = Self>
impl std::ops::Sub for Combined 
impl<T> std::ops::SubAssign<T> for Combined where Self: std::ops::Sub<T, Output = Self>
1 Like

You can get most of the way if you impl<T> Add<T> for Cost where T: Into<Cost>.

1 Like

Is that any different from impl<T: Into<Cost>> Add<T> for Cost ?

No. Both inline bounds and where bounds are the exact same.