Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to categorize over units of measure?

Tags:

f#

The problem is simple, I wish to do some calculations on some travel expenses which include both expenses in DKK and JPY. Thus I've found a nice way to model currency so I am able to convert back and forth:

[<Measure>] type JPY
[<Measure>] type DKK
type CurrencyRate<[<Measure>]'u, [<Measure>]'v> = 
    { Rate: decimal<'u/'v>; Date: System.DateTime}

let sep10 = System.DateTime(2015,9,10)
let DKK_TO_JPY : CurrencyRate<JPY,DKK> =
    { Rate = (1773.65m<JPY> / 100m<DKK>); Date = sep10}
let JPY_TO_DKK : CurrencyRate<DKK,JPY> =
    { Rate = (5.36m<DKK> / 100.0m<JPY>); Date=sep10 }

I proceed to model expenses as a record type

type Expense<[<Measure>] 'a> = {
    name: string
    quantity: int
    amount: decimal<'a>
}

and here I have an example list of expenses:

let travel_expenses = [
    { name = "flight tickets"; quantity = 1; amount = 5000m<DKK> }
    { name = "shinkansen ->"; quantity = 1; amount = 10000m<JPY> }
    { name = "shinkansen <-"; quantity = 1; amount = 10000m<JPY> }
]

And this is where the show stops... F# doesn't like that list, and complaints that all of the list should be DKK, -which of course makes sense.

Then I thought that there must be some smart way to make a discriminated union of my units of measures to put them in a category, and then I attempted with:

[<Measure>] type Currency = JPY | DKK

But this is not possible and results in The kind of the type specified by its attributes does not match the kind implied by its definition.

The solution I've come up with so far is very redundant, and I feel that it makes the unit of measure quite pointless.

type Money =
    | DKK of decimal<DKK>
    | JPY of decimal<JPY>
type Expense = {
    name: string
    quantity: int
    amount: Money
}
let travel_expenses = [
    { name = "flight tickets"; quantity = 1; amount = DKK(5000m<DKK>) }
    { name = "shinkansen ->"; quantity = 1; amount = JPY(10000m<JPY>) }
    { name = "shinkansen <-"; quantity = 1; amount = JPY(10000m<JPY>) }
]

Is there a good way of working with these units of measures as categories? like for example

[<Measure>] Length = Meter | Feet
[<Measure>] Currency = JPY | DKK | USD

or should I remodel my problem and maybe not use units of measure?

like image 648
Michelrandahl Avatar asked Sep 10 '15 15:09

Michelrandahl


2 Answers

Regarding the first question no, you can't but I think you don't need units of measures for that problem as you state in your second question.

Think how do you plan to get those records at runtime (user input, from a db, from a file, ...) and remember units of measures are a compile-time features, erased at runtime. Unless those records are always hardcoded, which will make your program useless.

My feeling is that you need to deal at run-time with those currencies and makes more sense to treat them as data.

Try for instance adding a field to Expense called currency:

type Expense = {
    name: string
    quantity: int
    amount: decimal
    currency: Currency
}

then

type CurrencyRate = {
    currencyFrom: Currency
    currencyTo: Currency
    rate: decimal
    date: System.DateTime}
like image 100
Gus Avatar answered Sep 27 '22 15:09

Gus


As an alternative to Gustavo's accepted answer, If you still want to prevent anybody and any function accidentally summing JPY with DKK amounts, you can keep your idea of discriminated union like so :

let sep10 = System.DateTime(2015,9,10)

type Money =
    | DKK of decimal
    | JPY of decimal

type Expense = {
    name: string
    quantity: int
    amount: Money
    date : System.DateTime
}

type RatesTime = { JPY_TO_DKK : decimal ; DKK_TO_JPY : decimal ; Date : System.DateTime}

let rates_sep10Tosep12 = [
    { JPY_TO_DKK = 1773.65m ; DKK_TO_JPY = 5.36m ; Date = sep10}
    { JPY_TO_DKK = 1779.42m ; DKK_TO_JPY = 5.31m ; Date = sep10.AddDays(1.0)}
    { JPY_TO_DKK = 1776.07m ; DKK_TO_JPY = 5.33m ; Date = sep10.AddDays(2.0)}
    ]

let travel_expenses = [
    { name = "flight tickets"; quantity = 1; amount = DKK 5000m; date =sep10 }
    { name = "shinkansen ->"; quantity = 1; amount = JPY 10000m; date = sep10.AddDays(1.0)}
    { name = "shinkansen <-"; quantity = 1; amount = JPY 10000m ; date = sep10.AddDays(2.0)}
]

let IN_DKK (rt : RatesTime list) (e : Expense) = 
    let {name= _ ;quantity = _ ;amount = a ;date = d} = e
    match a with
    |DKK x -> x
    |JPY y -> 
         let rtOfDate = List.tryFind (fun (x:RatesTime) -> x.Date = d) rt
         match rtOfDate with
         | Some r -> y * r.JPY_TO_DKK
         | None -> failwith "no rate for period %A" d

let total_expenses_IN_DKK = 
    travel_expenses
    |> List.fold(fun acc e -> (IN_DKK rates_sep10Tosep12 e) + acc) 0m 

Even better would be to make function IN_DKK as a member of type Expense and put a restriction (private,...) on the field "amount". Your initial idea of units of measure makes sense to prevent summing different currencies but unfortunately it does not prevent from converting from one to another and back to the first currency. And since your rates are not inverse (r * r' <> 1 as your data shows), unit of measure for currencies are dangerous and error prone. Note : I did not take into account the field "quantity" in my snippet.

like image 43
FZed Avatar answered Sep 27 '22 15:09

FZed