11
\$\begingroup\$

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
\$\endgroup\$
  • \$\begingroup\$ I'm not sure this example really addresses the point of that article. It looks like all operations should be valid whether you're pricing by unit or weight, and whichever currency you're using. So I'd have thought an OO type system could capture your requirements just as well- if not better \$\endgroup\$ – Ben Aaronson Nov 7 '14 at 10:58
  • 1
    \$\begingroup\$ It's not a question of whether you can represent the requirements in an OO style or not but rather how much code is needed, how well it reads and how error prone it is. For example, if I add a new currency type above, I will get compiler warnings if I forget to add the proper matches. Also, I find that the first code listing provides a very good overview of the concepts. \$\endgroup\$ – Morten Christiansen Nov 7 '14 at 11:40
  • \$\begingroup\$ Does the domain model assume that a conversion rate of currency a to b will be the inverse of the conversion rate of currency b to a? \$\endgroup\$ – Ben Aaronson Nov 7 '14 at 17:25
  • \$\begingroup\$ Not necessarily. I haven't really explored the concept, but it is conceivable that a store might want to use specific conversion rates. \$\endgroup\$ – Morten Christiansen Nov 10 '14 at 7:19
  • \$\begingroup\$ I wouldn't necessarily consider negatives as a downside here, people can return/exchange/refund things right? \$\endgroup\$ – Maslow Feb 3 '16 at 13:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.