Type-Safe Currency Conversion with F# Units of Measure
Date: 2024-10-10 | create | tech | fsharp | types |
I'm building Cloud Compare - a site that makes it easy to compare different cloud offerings. I quickly ran into an issue with currency conversion - many clouds price in USD but several (like Hetzner) price in other currencies.
So here I wanted to share an approach to currency conversion that offers a more type-safe experience by leveraging F#'s units of measure feature.
F# Units of Measure
F# has a feature called units of measure. This allows you to add a "unit" to a number value, providing more context to what that number represents. This is useful to you the programmer but it also allows the type system to help you when you make a mistake: "I don't think you can divide float<inches>
by float<kg>
"
In most languages people build around this by either:
- Having a separate field to hold the unit -
amount
andamountType
- Having a special object that handles this - AmountDto(
amount
,amountType
)
But generally these approaches are all ways to try and add additional info to that base int
/ float
/ decimal
so we know what it represents.
Common examples:
- Weight - lbs, g, kg
- Length - inches, cm, feet
- Currencies - USD, euro, kronor
Units of measure is just a way to tag this extra info - but do it in such a way that the type system itself can understand it (and thus yell at you at compile time when you do it wrong).
From the MS docs you can define a measure like this:
[<Measure>] type cm
And also define unit conversions like this (here 1 ml
== a cubic centimeter cm^3
)
[<Measure>] type ml = cm^3
F# Units of Measure for Type-Safe Currency Conversion
For my purposes I want to be able to store all my cloud prices in the currency they're sold in and present them in a currency that makes sense for the user. This makes it easier for me because I can just copy over the price directly (vs trying to convert each time) - and I can render them to the correct price to the end user based on current currency conversion rates.
To do this:
- Create Measures for usd and euro
- Create Currency to hold all the currencies I accept
- Create a currency convertor - here currency -> usd
[<Measure>] type usd
[<Measure>] type euro
type Currency =
| Usd of float<usd>
| Euro of float<euro>
let currencyToUsd
(currency: Currency)
: float<usd>
=
match currency with
| Currency.Usd s -> s
| Currency.Euro e ->
// 2024.09.16 - 1 euro : 1.11 dollars
(e * 1.11<usd/euro>)
Together this allows me to list prices in their native currency then get type-safe conversion back into USD which is what I display on my site. Plus when I need to update the currency conversions - I can just do that in my central conversion function.
Next
There are probably better ways to do this conversion stuff but I saw an opportunity to try out units of measure and I'm p happy with it so will keep it for now.
If you liked this post you might also like:
Want more like this?
The best / easiest way to support my work is by subscribing for future updates and sharing with your network.