Parsing PhantomData

Hey,

i think what i want to achieve is not possible, but i want to ask anyway.

I was thinking about creating a currency system that uses PhantomData to prevent applying +/- between different currencies.

So basically having a Currency trait that is implemented by each currency struct providing static functions as returning iso code for example.

Then i'm having a monetary value struct that is bound to a currency with PhantomData. Like

struct Money<T: Currency> {
  amount: i64,
  phantom: PhantomData<T>
}

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?

Yeah, just add an associated constant to the Currency trait giving the appropriate string: EUR, USD etc.

Pseudocode:

trait Currency {
    const NAME: &'static str;
}

impl<T: Currency> FromStr for Money<T> {
    type Err = ParseMoneyError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self::from_amount(s.strip_suffix(T::NAME)?.parse()?))
    }
}

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

What do you expect the compiler to do for code like the following? Should it allow it or disallow it?

let money1 = parse_any_currency(string1);
let money2 = parse_any_currency(string2);
let sum = money1 + money2;
1 Like

Yes as i said in introduction, i'm pretty sure this not possible.

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:

#[derive(Debug)]
enum Currency {
    EUR,
    USD,
    GBP,
    JPY
}

struct Money {
    value: i64,
    currency: Currency
}

impl Money {
    pub fn parse(s: impl std::fmt::Display) -> Money {
        // parse as needed
        Money {
            value: 100,
            currency: Currency::USD
        } // as an example
    }
}

fn main() {
    let usd = Money::parse("hey");
    println!("{} {:?}", usd.value, usd.currency);
}

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.

That is exactly what i had before, but then i wanted to try different options using the type system

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.

2 Likes

Using those kind of types would only make sense if you knew the kind of currency in advance:

fn main() {
  let text = "100 USD";
  let usd: Money<USD> = Money::parse(text); // fine
}

But if you need to do it dynamically (which you seem to require), that won't work.

Rust is "statically typed" for a reason.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.