Doubloon
This library implements a Money datatype that supports both a statically-typed
and dynamically-typed Currency. That is to say, you can create a Money<USD>
that is a totally different type than a Money<JPY>, or you can create a
Money<&dyn Currency> where the currency is determined at runtime, but still
safely do math with it (i.e., Money<&dyn Currency> + Money<&dyn Currency>
returns a fallible Result because the currencies might be different).
My main motivation for building this was to learn more about Rust trait bounds and custom operators. But I was also recently looking for a crate to represent an amount of money in a currency, and I noticed that the most popular one, rusty_money, hadn't been updated in a while (as of 2024---it's more active now). It also has a rather un-ergonomic API and set of behaviors: for example, it requires the use of explicit lifetimes (which naturally infect all types that use it), and it simply panics when you do math on instances with different currencies.
Although I'm fairly new to Rust, I felt like the powerful language features could support a better and more flexible experience, so I built something new, and learned a lot about Rust along the way!
Requirements
I wanted a Money data type that offered the following features:
- Tracks the amount as a high-precision decimal: The standard floating point data types can't be used for monetary amounts because even simple addition can produce rather strange results. A common alternative is to track the amount in currency minor units (e.g., cents of USD), but this becomes awkward when a currency decides to change its number of minor units, as Iceland did in 2007. It also makes it difficult to represent fractional minor units, such as a stock price expressed in eighths of a cent, or a per-second usage price for a cloud resource.
- Supports instances with statically-typed currencies: In some applications
you know the currency at compile time, and you want to ensure that an amount
of
Moneyin one currency can't accidentally be passed to a function expecting an amount in a different currency. In other words, you wantMoney<USD>andMoney<JPY>to be totally different types, so that it becomes a compile error to mix them up. - Supports instances with dynamically-typed currencies: In other
applications, you don't know the currency until runtime, so we need to support
that as well. For example, you might get an API request with an amount and a
three-character currency code, so you need to lookup the currency in a map and
create a
Money<&dyn Currency>. - Allows equality comparisons: Regardless of whether the currency is statically or dynamically-typed, you should be able to test two instances for equality since that can never fail--they might be unequal, but the comparison is always a valid thing to do.
- Supports math operations in a safe way: If you add two
Money<USD>instances, you should get aMoney<USD>since the compiler ensures the currencies are the same. But if you add twoMoney<&dyn Currency>instances, or a mix of statically and dynamically-typed currencies, you should get aResultsince the operation could fail if the currencies are actually different. TheResulttype supports chaining through the.and_then()method, so one can still work with multiple terms in a safe way.
Amazingly, Rust's language features do make all of this possible! In the rest of this README, I'll explain how I made this work, and discuss a few of the approaches I tried that didn't quite work.
Currency Trait and Implementations
The first step was to define a Currency trait that all currencies must
implement. I kept it simple for now, but one could expand this in the future to
include other details:
/// Common trait for all currencies.
Initially I didn't include &self as an argument on these methods because I
figured the implementations would just return static data, but this created a
problem when I tried to build a reference to a dynamically-typed currency:
&dyn Currency. To do this, Rust requires the trait to be "object safe," which
means the compiler can build a v-table and do dynamic dispatch. Without a
reference to &self, there would be no way to know which implementation of the
trait method to call at runtime, so &self must be an argument, even if you
never refer to it in your implementations.
For instances of Currency my first inclination was to declare an enum with
the code as the variant name, as that must be unique. But in Rust an enum is a
type and the variants of that enum are all instances of the same type. So if I
declared the currencies as something like enum CurrencySet all the money
instances would end up being Money<CurrencySet>, which would defeat our desire
to support statically-typed currencies. The same would be true if I declared
just one CurrencyImpl struct and declared constant instances of it for the
various currencies.
Instead, we need each Currency implementation to be it's own type. The
easiest way to do that is to declare them as separate structs, each of which
impl Currency:
/// US Dollar
;
/// Yen
;
Declaring USD and JPY as separate structs makes them separate types,
which will enable us to create statically-typed Money<USD> vs Money<JPY>.
Money Type
Now that we have some currencies defined, we can build our Money type:
use Decimal;
/// An amount of money in a particular currency.
We define a generic type argument C for the currency, but notice that I don't
add a trait bound here in the struct definition. That is, I just declare
Money<C> not Money<C: Currency>. When I first started learning Rust I tended
to add trait bounds on my struct definitions, but I realized this was both
unnecessary and restrictive. Since you must add trait bounds on the impl
blocks when referring to trait methods, and because they only way to create or
interact with the type is through methods defined in the impl blocks, it's
typically unnecessary to add trait bounds on the struct itself. But it's also
overly restrictive: we don't want to restrict C to be only a Currency as we
also want to support an &dyn Currency or maybe even a Box<dyn Currency>. We
can do that using separate impl blocks with different trait bounds and types
for C.
At first I tried to construct a single impl block with a trait bound that
allowed either an owned Currency implementation OR a reference to a dynamic
Currency, but that doesn't actually make sense, as an &dyn Currency is
actually a type not a trait, so it can't be used as a trait bound. But it
can be used as the type for a generic type argument in a separate impl block,
which you'll see below.
I also considered implementing Currency for &dyn Currency, which is possible
in Rust, but that would erase the distinction between the two: it would then be
possible to use a Money<&dyn Currency> in an impl block with a trait bound
of C: Currency and the code couldn't really tell the difference between a
statically and dynamically-typed Currency.
So I started with an impl block with no trait bounds, containing methods that
don't really care what the type of C actually is:
/// Common functions for statically and dynamically-typed currencies.
The new() and amount() method don't really need to know what type C
actually is, so we can define them once. This does have an interesting drawback,
however: one can pass any type for the currency argument, so one could
construct a Money<String> or Money<Foo> where Foo is not a Currency.
Although that's strange, it's probably fine since you can't do much with that
Money instance without calling methods defined in the other impl blocks,
which will establish bounds on the type of C. But if you find this
distasteful, see the "Marker Trait for New" section below for an interesting
solution.
Statically-Typed Currencies
The next impl block defines methods that are specific to owned
statically-typed Currency instances:
/// Functions specifically for owned statically-typed Currency instances.
Here we add a trait bound on C of Currency + Copy, meaning that whatever the
caller is using for C it must be an owned Currency instance that also
supports copy semantics. This allows us to return a copy of the Currency
instance from the currency() method. Since USD and JPY are unit structs,
copying them doesn't require any significant work, so it's fine and convenient
to just return a copy instead of a reference.
Now we can create Money instances with a statically-typed Currency:
// m_usd is type Money<USD>
let m_usd = new;
assert_eq!;
assert_eq!;
// m_jpy is type Money<JPY>
let m_jpy = new;
assert_eq!;
assert_eq!;
// This won't even compile because they are totally different types
// assert_eq!(m_usd, m_jpy);
Dynamically-Typed Currencies
To support references to dynamically-typed currencies, we can add another impl
block where we provide a specific type for the generic C type argument:
/// Functions specifically for borrowed dynamically-typed currencies.
There are a few subtleties to note here. First, we can't do this with a trait
bound like we did above because &'c dyn Currency is a type not a trait.
But that's okay because we can simply use that as the explicit type for C in
this impl block.
Second, we declare a lifetime argument 'c for the impl block, and use that
as the lifetime of the Currency references. This will make compiler enforce
that the Currency instance lives for at least as long as the Money instance
does, which is good because we are holding a reference to it. Thankfully,
callers won't have to deal with this lifetime argument in their code, as the
compiler can work it out from context. One will be able to simply do something
like this:
// CURRENCIES is a HashMap<'static str, &'static dyn Currency>
// so dynamic_currency is of type `&dyn Currency`
let dynamic_currency = CURRENCIES.get.unwrap;
// money is of type `Money<&dyn Currency>`
let money = new;
assert_eq!;
let other_money = new;
assert_eq!;
Third, you might be surprised that we can declare another method with the same
name as the method we just declared in the previous impl block. Rust allows
this for methods that take &self as an argument because it can use that to
determine the correct implementation. And in this case we can redefine the
return type to be the same reference we are holding rather than a copy of an
owned statically-typed Currency value.
The implication here is that methods that do not take &self as an argument
cannot be "overloaded" (so to speak). For example, I initially tried defining
different versions of the new() method in the different impl blocks, the
first taking an owned Currency value and the second taking a &dyn Currency
reference, but Rust doesn't currently allow that: the declaration will work, but
when you try to use one of those new() methods you'll get an error saying
there are multiple candidates and it can't figure out which one you want to
call. There might be a syntax to disambiguate, but I couldn't figure it out,
which means my callers probably won't be able to either.
Supporting Safe Money Math
We can now create Money instances with static or dynamically-typed Currencies,
so let's make it possible to add them in a safe way.
Money<USD> + Money<USD>should returnMoney<USD>since that's infallible (though it can still overflow).Money<USD> + Money<JPY>shouldn't even compile.Money<&dyn Currency> + Money<&dyn Currency>should return aResultsince the currencies might be different.Money<USD> + Money<&dyn Currency>andMoney<&dyn Currency> + Money<USD>should also be possible, returning aResultwith theOktype being whatever the left-hand side's type was.
Amazingly, Rust makes all of this possible. The Add trait not only allows you
to specify a different type for the right-hand side term, but also for the
Output of the operation!
The statically-typed implementation is pretty straightforward:
/// Adds two Money instances with the same statically-typed currencies.
/// Attempting to add two instances with _different_ statically-typed
/// Currencies simply won't compile.
For the dynamically-typed version, we define a MoneyMathError enum and set the
Output associated type to be a Result<Self, MoneyMathError>:
/// Errors that can occur when doing math with Money instances that
/// have dynamically-typed currencies
/// Adds two Money instances with dynamically-typed currencies.
/// The Output is a Result instead of a Money since the operation
/// can fail if the currencies are incompatible.
We again specify &'c dyn Currency as the explicit type for the generic type
argument because it's a type, not a trait, so we can't express it as a trait
bound. We also check whether the currencies are the same, and return an error if
they are not.
Supporting a mix of statically and dynamically-typed currencies is also possible
by specifying the right-hand side type in the Add trait (it defaults to
Self):
/// Adds a Money instance with a statically-typed Currency to
/// a Money instance with a dynamically-typed Currency. The output
/// is a Result since the operation can fail if the currencies are
/// incompatible.
/// Adds a Money instance with a dynamically-typed Currency to
/// a Money instance with a statically-typed Currency. The Output
/// is a Result since the operation can fail if the currencies are
/// incompatible.
With all of this we can now do Money math like so:
// statically-typed
assert_eq!;
// dynamically-typed, same currency -> Ok
let currency_usd = CURRENCIES.get.unwrap;
assert_eq!;
// dynamically-typed, different currencies -> Err
let currency_jpy = CURRENCIES.get.unwrap;
assert_eq!;
// dynamically-typed + statically-typed, same currency -> Ok(dynamically-typed)
assert_eq!;
// dynamically-typed + statically-typed, different currencies -> Err
assert_eq!;
// statically-typed, multi-term
assert_eq!;
// dynamically-typed, multi-term using Result::and_then()
// (if an error occurs, closures are skipped and final result is an error)
assert_eq!;
In the actual code, there are macros defined for binary and unary ops, which
makes it trivial for Money to also support subtraction, multiplication,
division, remainder and negation as well using the same techniques.
Equality Comparisons
The asserts above rely on the ability to compare Money instances for equality,
which requires implementing the PartialEq trait for both statically and
dynamically-typed currencies:
/// Allows equality comparisons between Money instances with statically-typed
/// currencies. The compiler will already ensure that `C` is the same for
/// both instances, so only the amounts must match.
/// Allows equality comparisons between Money instances with dynamically-typed
/// currencies. Both the amount and the currency codes must be the same.
Just as with the math operations, we can also support comparing a mix of
statically and dynamically-typed currencies by specifying the right-hand side
type in the PartialEq trait (defaults to Self):
/// Allows equality comparisons between Money instances with dynamically-typed
/// currencies and those with statically-typed currencies
/// Allows equality comparisons between Money instances with dynamically-typed
/// currencies and those with statically-typed currencies
The same technique (more or less) is used to support PartialOrd. That trait
allow you to return None if the two instances are incomparable, which is what
we return when the dynamically-typed currencies are different.
Formatting
This crate also offers an optional "formatting" feature. When enabled, it uses the icu crate for locale-aware formatting.
Currency formatting in the icu crate is still experimental, so use this
feature at your own risk. Once it becomes stable, this crate will be updated to
use the stable version.
Locale-aware formatting takes into account both the currency and the locale of the user to which you will display the formatted monetary amount. The formatting rules come from the Unicode Common Locale Data Repository (CLDR), which is maintained by native speakers.
use locale;
let m = new;
// en-US uses comma for group separator, period for decimal separator,
// with the symbol at the left with no spacing.
assert_eq!;
// ir-IR is like en-US except there is a narrow non-breaking space
// between the symbol and the amount.
assert_eq!;
// tr-TR is similar to ir-IR but uses period for the group separator
// and comma for the decimal separator.
assert_eq!;
// fr-FR puts the symbol at the end, and uses non-breaking spaces
// between digit groups, comma as a decimal separator,
// and a narrow non-breaking space between the amount and symbol.
assert_eq!;
Serde
The library also has support for serde serialization via
the optional serde feature.
When serializing a Money instance, it will write a struct with two fields: the
amount as a string, and the currency code as a string. For example, serializing
a Money::new(Decimal::ONE, USD) to JSON yields the following:
Unfortunately serde deserialization doesn't support any sort of caller-supplied
context, so there's no generic way for this library to turn a serialized
currency code back into the appropriate &dyn Currency. Since callers may
implement their own Currency instances to support application-specific
currencies, there's no single well-known global map the library could use to
resolve a currency code.
To support deserialization, your application should deserialize into a struct like this:
You can then resolve the code to the appropriate &dyn Currency and construct
a Money instance using that.
Marker Trait for New
When we first saw the Money::new() method, I noted that it technically allows
one to construct a Money with something that isn't actually a Currency. At
first I tried to work around this by putting new() into the specific impl
blocks like so, but this doesn't compile:
// DOES NOT COMPILE!
I'm not sure why the compiler can't figure out which version of new() to call
given that the argument types are different, but it doesn't work for now.
Although we can't construct a single trait bound that allows either an owned
implementation of Currency or a reference to a dynamic one, we can define a
new trait and do a blanket implementation for those two things. For example:
// New marker trait, with blanket implementations for anything that
// implements Currency, and any `&'c dyn Currency`
// Single impl block using CurrencyOrRef as trait bound
Now it's impossible to construct a Money<String> or Money<Foo> where Foo
is not a Currency. But it's also not impossible for a caller to just implement
the CurrencyOrRef marker trait on their own Foo type, so it's unclear to me
if this is really worth it in the end.
But this technique does make it easier to support other kinds of constructors
that might need a subset of the Currency trait. For example, say we wanted to
support creating a Money from some amount of currency minor units. To do that,
we need to know how many minor units the currency supports, which is a method on
the Currency trait. We could do that by making the marker trait here a bit
smarter:
/// Used as a trait bound when constructing new instances of Money
/// from minor units.
/// Blanket implementation for any static [Currency] instance.
/// Implementation for an `&dyn Currency`.
/// Methods that require knowing the `minor_units` of the currency.
This makes the marker trait a bit more useful and perhaps worth it.
Corrections or Suggestions?
Is there a better way to do this? I'm fairly new to Rust, so perhaps there's a mechanism I haven't run across yet that would provide a better solution. If you know of something, please open an issue and tell me about it! I'll update the code and README accordingly.