I have created some custom arithmetic types. Can I use generics to reduce code duplication?

Hi everyone!

I have an issue where I try to create a generic function for an arithmetic operation of custom types. There is a TLDR at the end.

Initial Situation:

So I have created some custom types:

  • Money
  • Percentage

Percentage :nerd_face::

use std::fmt;
use std::ops::Add;

// Our Percentage Type
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Percentage {
    pub value: f64
}

impl Percentage {
    pub fn new(value: f64) -> Percentage {
        Percentage { value: value}
    }
}

impl fmt::Display for Percentage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}%", self.value)
    }
}

// Implement operations for Percent

// 10% + 23% = 33%
impl Add<Percentage> for Percentage {
    type Output = Percentage;

    fn add(self, rhs: Percentage) -> Self::Output {
        Percentage::new(self.value + rhs.value)
    }
}

// 100.0 + 10% = 110.0
impl Add<Percentage> for f64 {
    type Output = f64;

    fn add(self, rhs: Percentage) -> Self::Output {
        self + (self * rhs.value / 100.0)
    }
}

impl Add<f64> for Percentage {
    type Output = f64;

    fn add(self, rhs: f64) -> Self::Output {
        rhs.add(self)
    }
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add() { // 15% + 25% = 37%
        assert_eq!(Percentage::new(15.0) + Percentage::new(22.0), Percentage { value: 37.0});
    }

    #[test]
    fn add_f64() { // 100.0 + 15% = 115.0
        assert_eq!(Percentage::new(15.0) + 100.0, 115.0);
    }
}

Money :dollar::

use std::fmt;
use std::ops::Add;

use crate::Percentage;

// Currency Type
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum Currency {
    Euros,
    Dollars
}

// Money Type
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Money {
    pub amount: f64,
    pub currency: Currency
}

impl Money {
    pub fn new(amount: f64, currency: Currency) -> Money {
        Money {amount: amount, currency: currency}
    }
}

// 10€ + 20€ = 30€
impl Add<Money> for Money {
    type Output = Money;

    fn add(self, rhs: Money) -> Self::Output {
        if self.currency == rhs.currency {
            Money { 
                amount: self.amount + rhs.amount,
                currency: self.currency
            }
        } else {
            todo!() // implement some conversion operation
                    // but let's keep this simple for now
        }
    }
}

// 10€ + 5.0 = 15€
impl Add<f64> for Money {
    type Output = Money;

    fn add(self, rhs: f64) -> Self::Output {
        Money::new(self.amount + rhs, self.currency)
    }
}

// Implement Percentage operations
// 100€ + 15% = 115€
impl Add<Percentage> for Money {
    type Output = Money;

    fn add(self, rhs: Percentage) -> Self::Output {
        Money {
            amount: self.amount + (self.amount * rhs.value / 100.0),
            currency: self.currency
        }
    }
}

impl fmt::Display for Money {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.currency {
            Currency::Dollars => write!(f, "${:.2}", self.amount),
            Currency::Euros => write!(f, "{:.2}€", self.amount)         
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_f64 () { // 100€ + 42€ = 142€
        assert_eq!(Money::new(100.0, Currency::Euros) + 42.0, Money{amount: 142.0, currency: Currency::Euros});
    }

    #[test]
    fn add_percent() { // 100€ + 30% = 130€
        assert_eq!(Money::new(100.0, Currency::Euros) + Percentage::new(30.0), Money {amount: 130.0, currency: Currency::Euros});
    }   
}

Addition of the custom types

I can add them without any issue.

mod units;
use units::percentage::Percentage;
use units::money::{Money, Currency};

fn main() {
    println!("42€ + 12%: {}", Money::new(42.0, Currency::Euros) + Percentage::new(12.0));
    // 42€ + 12%: 47.04€
}

One variable to rule them all :man_mage:

Next, what I wanted to store in a variable, either a default type:

  • f64
  • i32

Or one of my custom types:

  • Percentage
  • Money

My solution for this is to define an enum ResType that can store all the types I want to use. And implement Add for ResType.

mod units;
use units::percentage::Percentage;
use units::money::{Money, Currency};

use std::ops::Add;

#[allow(unused)]
#[derive(Debug, Clone, Copy)]
enum ResType {
    Percent(Percentage),
    Money(Money),
    Float(f64)
}

impl ResType {
    fn is_percent(self) -> bool {
        matches!(self, ResType::Percent(_))
    }

    fn is_money(self) -> bool {
        matches!(self, ResType::Money(_))
    }

    fn is_float(self) -> bool {
        matches!(self, ResType::Float(_))
    }
}

// Priority of types:
// Money > Float > Percent
// 5€ > 3.14 > 1 %

impl Add<ResType> for ResType {
    type Output = ResType;

    fn add(self, rhs: ResType) -> Self::Output {
        
        // if one of the types is Money, we return some Money
        if self.is_money() || rhs.is_money() {
            let (money, other) = match (self, rhs) {
                (a, b) if a.is_money() => (a, b),
                (a, b) if b.is_money() => (b, a),
                _ => unreachable!("Either a or b is of type money.")
            };

            if let ResType::Money(m) = money {
                let result = match other {
                    ResType::Money(m2) => ResType::Money(m + m2),
                    ResType::Float(f) => ResType::Money(m + f),
                    ResType::Percent(p) => ResType::Money(m + p)
                };

                return result;
            }
        }

        // if one of the types is Float, we return a Float
        if self.is_float() || rhs.is_float() {
            let (float, other) = match (self, rhs) {
                (a, b) if a.is_float() => (a, b),
                (a, b) if b.is_float() => (b, a),
                _ => unreachable!("Either a or b is of type float.")
            };

            if let ResType::Float(f) = float {
                let result = match other {
                    ResType::Float(f2) => ResType::Float(f + f2),
                    ResType::Percent(p) => ResType::Float(f + p),
                    _ => unreachable!("ResType::Money was handled earlier.")
                };

                return result;
            }
        }

        // The only case left is: we have 2 percentages
        if self.is_percent() &&  rhs.is_percent() {
            if let (
                ResType::Percent(p1),
                ResType::Percent(p2)
            ) = (self, rhs) {
                return ResType::Percent(p1 + p2)
            }
        }

        unreachable!("All possible ResType combination have been handeled.")
    }
}


fn main() {
    let a = ResType::Money(Money::new(110.0, Currency::Euros));
    let b = ResType::Percent(Percentage::new(11.0));

    // 110€ + 11%
    let res = a + b;
    if let ResType::Money(m) = res {
        println!("{}", m);
    }
}

Lots of code duplication

The thing that I am not really happy about. Is that if I want to implement some other arithmetic operation, like Sub.
I write nearly the same code, except that the + is replaced with -.

            if let ResType::Money(m) = money {
                let result = match other {
                    ResType::Money(m2) => ResType::Money(m + m2),
                    ResType::Float(f) => ResType::Money(m + f),
                    ResType::Percent(p) => ResType::Money(m + p)
                };

                return result;
            }

became

            if let ResType::Money(m) = money {
                let result = match other {
                    ResType::Money(m2) => ResType::Money(m - m2),
                    ResType::Float(f) => ResType::Money(m - f),
                    ResType::Percent(p) => ResType::Money(m - p)
                };

                return result;
            }

And I have to duplicate it also for Mul and Div.

Generic functions to the rescue

In order to reduce code duplication.
I am trying to write a function that take an arithmetic operation as an argument.

arithmetic_operation(self, rhs, |x,y| x+y)

And we could use the same for Add, Sub, Mul, Div.

But I can't really find a way to write the signature of this function.

The furthest I got is writing a conversion for Money into ResType.

impl Into<ResType> for Money {
    fn into(self) -> ResType {
        ResType::Money(self)
    }
}

And starting to implement the function

impl ResType {
    // [...]

    fn arithmetic_operation<F, T>(self, rhs: ResType, op: F) -> ResType
    where
        T: Add<Output = Money>,
        F: Fn(Money, T) -> ResType,
    {
        // if one of the types is Money, we return some Money
        if self.is_money() || rhs.is_money() {
            let (money, other) = match (self, rhs) {
                (a, b) if a.is_money() => (a, b),
                (a, b) if b.is_money() => (b, a),
                _ => unreachable!("Either a or b is of type money.")
            };

            if let ResType::Money(m) = money {
                let result = match other {
                    ResType::Money(m2) => op(m, m2), // <-----
                    ResType::Float(f) => ResType::Money(m + f),
                    ResType::Percent(p) => ResType::Money(m + p)
                };

                return result;
            }
        }

        todo!()
    }
}

Help!

But I always have an error where the type expected is T, and the function is given Money.

❯ cargo build
   Compiling type_system v0.1.0 (/home/olivier/projets/rust_experiments/type_system)
error[E0308]: mismatched types
  --> src/main.rs:43:49
   |
28 |     fn arithmetic_operation<F, T>(self, rhs: ResType, op: F) -> ResType
   |                                - expected this type parameter
...
43 |                     ResType::Money(m2) => op(m, m2),
   |                                           --    ^^ expected type parameter `T`, found `Money`
   |                                           |
   |                                           arguments to this function are incorrect
   |
   = note: expected type parameter `T`
                      found struct `Money`

TLDR

Money is a custom type that implements Add.
How should I modify the traits for T in this function signature?

    fn arithmetic_operation<F, T>(self, rhs: ResType, op: F) -> ResType
    where
        T: Add<Output = Money>,
        F: Fn(Money, T) -> ResType,
    {

If I want to solve this error message

❯ cargo build
   Compiling type_system v0.1.0 (/home/olivier/projets/rust_experiments/type_system)
error[E0308]: mismatched types
  --> src/main.rs:43:49
   |
28 |     fn arithmetic_operation<F, T>(self, rhs: ResType, op: F) -> ResType
   |                                - expected this type parameter
...
43 |                     ResType::Money(m2) => op(m, m2),
   |                                           --    ^^ expected type parameter `T`, found `Money`
   |                                           |
   |                                           arguments to this function are incorrect
   |
   = note: expected type parameter `T`
                      found struct `Money`

If some of you want to test this code. I have it on Github.

git clone https://github.com/0xfalafel/rust_experiments.git -b forum
cd rust_experiments/type_system
cargo build

Thanks, and sorry for the big wall of text.

You’re getting an error in arithmetic_operation because it can be called like so:

arithmetic_operation::<fn(Money, f64) -> ResType, f64>(/* ... */)

This satisfies the constraints in the function signature[1], but you pass a Money to the second argument of the function. You want some type of for<T> fn(), but HRTBs are only valid with lifetimes[2].

I would do this with a macro:

macro_rules! impl_binop_for_restype {
    ($trait_name:ident $op:tt) => {
// Yes this isn’t formatted properly but I’m not putting
// in indentation for each line.

// Priority of types:
// Money > Float > Percent
// 5€ > 3.14 > 1 %

impl $trait_name<ResType> for ResType {
    type Output = ResType;

    fn add(self, rhs: ResType) -> Self::Output {
        
        // if one of the types is Money, we return some Money
        if self.is_money() || rhs.is_money() {
            let (money, other) = match (self, rhs) {
                (a, b) if a.is_money() => (a, b),
                (a, b) if b.is_money() => (b, a),
                _ => unreachable!("Either a or b is of type money.")
            };

            if let ResType::Money(m) = money {
                let result = match other {
                    ResType::Money(m2) => ResType::Money(m $op m2),
                    ResType::Float(f) => ResType::Money(m $op f),
                    ResType::Percent(p) => ResType::Money(m $op p)
                };

                return result;
            }
        }

        // if one of the types is Float, we return a Float
        if self.is_float() || rhs.is_float() {
            let (float, other) = match (self, rhs) {
                (a, b) if a.is_float() => (a, b),
                (a, b) if b.is_float() => (b, a),
                _ => unreachable!("Either a or b is of type float.")
            };

            if let ResType::Float(f) = float {
                let result = match other {
                    ResType::Float(f2) => ResType::Float(f $op f2),
                    ResType::Percent(p) => ResType::Float(f $op p),
                    _ => unreachable!("ResType::Money was handled earlier.")
                };

                return result;
            }
        }

        // The only case left is: we have 2 percentages
        if self.is_percent() &&  rhs.is_percent() {
            if let (
                ResType::Percent(p1),
                ResType::Percent(p2)
            ) = (self, rhs) {
                return ResType::Percent(p1 $op p2)
            }
        }

        unreachable!("All possible ResType combination have been handeled.")
    }
}
    }
}
impl_binop_for_restype!{Add +};
impl_binop_for_restype!{Sub -};

Disclaimer: I did not test this with your actual code.

Also it just feels wrong that $10 + 15% = $25. It should either be a type error or $11.5 (maybe $1.5?)


  1. f64: Add<f64, Output = Money>, fn(Money, f64) -> ResType: Fn(Money, f64) -> ResType. ↩︎

  2. for<'a> fn() ↩︎

1 Like

Thanks a lot.

I have never used a Rust macro before. It does seem like an elegant enough solution for this. I am going to look into it.

Also, $10 + 15% should produce $11.50 (if I am not mistaken).

It's adding 2 percentages that add them up directly: 10% + 15% = 25%.

What about $15 + 15%? That results in $16.5, which doesn't make much sense.

Honestly, I am just playing around with Rust's type system. It's not a big deal if it doesn't make much sense.

But it did give me an interesting case to figure out generics (and now macros apparently!).

1 Like

By the way, this might better be written using match with alternatives on a pair, no unreachable needed as compiler will check whether match is exhaustive for you:

    fn add(self, rhs: ResType) -> Self::Output {
        use ResType as R;
        match (self, rhs) {
            (R::Money(m), R::Money(m2)) => R::Money(m + m2),
            (R::Money(m), R::Float(f))
            | (R::Float(f), R::Money(m)) => R::Money(m + f),
            (R::Money(m), R::Percent(p))
            | (R::Percent(p), R::Money(m)) => R::Money(m + p),
            (R::Float(f), R::Float(f2)) => R::Float(f + f2),
            (R::Float(f), R::Percent(p))
            | (R::Percent(p), R::Float(f)) => R::Float(f + p),
            (R::Percent(p), R::Percent(p2)) => R::Percent(p + p2),
        }
    }

Playground

2 Likes