I'm trying to design a model for purchasing amounts of products in F#, following the ideas for designing for correctness (source). The idea is to use the type system to capture the different legal states, making it impossible to represent invalid states as defined by your requirements.
Since I'm not too experienced with F# yet I was hoping to get some feedback on my approach. Can the following be represented in a simpler manner and are there any problems with this design? This does obviously not support a dynamic list of currencies, so this would be an area of improvement. Another issue would be dealing with negative amounts which is not prevented.
// Monatary units
[<Measure>] type dkk
[<Measure>] type usd
// Amounts of products
[<Measure>]
type kg
[<Measure>]
type unit
type Weight = decimal<kg>
type Count = int<unit>
type Amount =
| Weight of Weight
| Count of Count
type Money =
| DKK of decimal<dkk>
| USD of decimal<usd>
type UnitPrice<[<Measure>]'u> =
| DkkPrAmount of decimal<dkk/'u>
| UsdPrAmount of decimal<usd/'u>
type WeightedProduct = { Name:string; UnitPrice:UnitPrice<kg> }
type UnitProduct = { Name:string; UnitPrice:UnitPrice<unit> }
type Product =
| WeightedProduct of WeightedProduct
| UnitProduct of UnitProduct
type AmountOfProduct =
| KgOfProduct of (WeightedProduct * Weight)
| CountOfProduct of (UnitProduct * Count)
Some examples of how you would use the types:
// Available products
let snickers = { Name = "Snickers"; UnitPrice = DkkPrAmount 12.95m<dkk/unit> }
let mars = { Name = "Mars"; UnitPrice = DkkPrAmount 11.95m<dkk/unit> }
let twix = { Name = "Twix Double Pack"; UnitPrice = DkkPrAmount 18.95m<dkk/unit> }
let flour:WeightedProduct = { Name = "Wheat Flour"; UnitPrice = DkkPrAmount 15.95m<dkk/kg> }
let calculatePrice amountOfProduct =
match amountOfProduct with
| KgOfProduct (product, weight) ->
match product.UnitPrice with
| DkkPrAmount dkPrice -> DKK (weight * dkPrice)
| UsdPrAmount usPrice -> USD (weight * usPrice)
| CountOfProduct (product, count) ->
match product.UnitPrice with
| DkkPrAmount dkPrice -> DKK ((decimal count) * (dkPrice * 1.0m<unit>))
| UsdPrAmount usPrice -> USD ((decimal count) * (usPrice * 1.0m<unit>))
let calculateTotalPrice purchase priceConverter =
purchase
|> List.map calculatePrice
|> List.map priceConverter
|> List.sum
let dkkPerUsd = 6.23m<dkk/usd>
let usdPerDkk = 0.16m<usd/dkk>
let toDkk money =
match money with
| DKK dkk -> dkk
| USD usd -> dkkPerUsd * usd
let toUsd money =
match money with
| DKK dkk -> usdPerDkk * dkk
| USD usd -> usd
let purchase = [CountOfProduct (snickers, 1<unit>); CountOfProduct (mars, 1<unit>); CountOfProduct (twix, 3<unit>); KgOfProduct (flour, 20.0m<kg>)]
let totalPrice = calculateTotalPrice purchase toDkk