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 :
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 :
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
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.