This way the compiler ensures that i cannot add 5 EUR to 10 USD which is nice. But now imagine that i'm reading a file containing strings like "5EUR" or "10USD" which i want to parse into Money. I can do this easily when i know that i want to parse EUR, but given i don't know the currency inside the string, is this somehow possible?
Edit: Wait, I think I misunderstood. You want to make it dynamic? In that case:
One possibility is making the Currency trait support being an enum, by moving away from associated constants and self-less functions, and making everything take &self. Then, instead of storing a PhantomData<T> just store a T and use that. This will allow you to make an enum AnyCurrency and implement Currency for that. Then you can parse into a Money<AnyCurrency>.
However that approach doesn't work always - sometimes Money might need the information of which currency it holds statically. In that case, you might want to make a separate type holding money with any currency, or an enumeration of Money<Usd> and Money<Eur>.
Yeah the dynamical part was the key. I used to do it with enums, but in this case i need to check the currency in the +/- operations. Which i tried to avoid and let the compiler handle this
PhantomData wasn't designed to be used for such use case - it's just a marker to help your compiler understand the kind of rules that need to be applied for your type, in terms of borrowing checking and not.
In your case, the regular enums are much more suitable:
With those, you can overload the addition operation:
use anyhow::Result; // 1.0.43
use std::ops::{Add, Sub};
impl Add for Money {
type Output = Result<Self>;
fn add(self, other: Self) -> Self::Output {
if self.currency == other.currency {
Ok(Self {
value: self.value + other.value,
currency: self.currency
})
} else {
anyhow::bail!("currency mismatch")
}
}
}
fn main() -> Result<()> {
let usd_100 = Money::parse("hey");
let other_usd_100 = Money::parse("hey");
let added = (usd_100 + other_usd_100)?;
println!("{} {:?}", added.value, added.currency);
Ok(());
And if you want, creating a cumulative sum function to iterate over a list of Money is also possible by implementing such a function for any Iterator with Item-s being your Money.
This is precisely why you should not use PhantomData for type parameters that will, most of the time, be marker structs anyway.
If you had simply written
struct Money<T> { // don't write bounds on structs but that's another rant
amount: i64,
currency: T,
}
then you could still have zero-overhead type-safe arithmetic when T is a marker struct (like struct Eur;) but you could also write enum Dynamic { Eur, Usd } and use Money<Dynamic> for parsing when you didn't know the currency in advance. Another neat trick is Box<Money<dyn Currency>>. Using PhantomData buys you exactly nothing, and prevents you from actually using dataful types for T when that is useful.