F# operator overloading for conversion of multiple different units of measure

f#, f#-3.0, units-of-measurement

Solution

Actually it is possible, there is a way to do this:

type [<Measure>] seconds     
type [<Measure>] minutes
type [<Measure>] hours

let seconds_per_minute = 60<seconds> / 1<minutes>
let minutes_per_hour = 60<minutes> / 1<hours>

let minutes_to_seconds minutes seconds = minutes * seconds_per_minute + seconds
let hours_to_minutes hours minutes = hours * minutes_per_hour + minutes

type D1 = D1
type D2 = D2

type Sum = Sum with
  static member inline ($) (Sum, _:^t when ^t: null and ^t: struct) = id
  static member inline ($) (Sum, b)              = fun _  _  a -> a + b
  static member        ($) (Sum, b:int<minutes>) = fun D1 _  a -> hours_to_minutes   a b
  static member        ($) (Sum, b:int<seconds>) = fun D1 D2 a -> minutes_to_seconds a b    

let inline (+) a b :'t = (Sum $ b) D1 D2 a

let duration = 1<hours> + 2<minutes> + 3<seconds>

But it's really hacky, I wouldn't recommend it.

UPDATE

Based on the comments here are some answers:

This technique uses overloads which are resolved at compile-time, so there is no performance penalty at run-time. It is based on what I wrote some time ago in my blog.

To add more overloads you will have to add more dummy parameters (`D3`, `D4`, ...) and eventually if you decide to add some overloads which conflicts with an existing one you may have to use a ternary operator `(?<-)` or a function call with explicit static member constraints. Here's a sample code.

I think I wouldn't use it since it requires many hacks (a Dummy overload and 2 dummy types) and the code becomes less readable. Eventually if F# adds more support for inline functions based on overloads I would definitely consider it.

Phil Trelford's technique (mentioned in Reed's answer) works at run-time, a 3rd option would be to use phantom types, it may require less hacks.

Conclusion

If I had to choose between all alternatives I would use this technique but being more explicit at the call site, I mean I would define conversion functions like `minutes`, `seconds` and that way at the call site I would write:

let duration = seconds 1<hours> + seconds 2<minutes> + 3<seconds>

And then to define those conversion functions I would use overloads, but it would be less hacky than re-defining an existing binary operator.

UPDATE 2

I just found a better solution:

type D1 = interface end
type D2 = interface end
type D3 = interface end

type Sum =
    class
        interface D1
        interface D2
        interface D3
    end

type Sum with
  static member inline ($) (_: D1, _: ^t when ^t: null and ^t: struct) = ()
  static member inline ($) (_: D1, b)               = fun a -> a + b
  static member        ($) (_: D2, b: int<minutes>) = fun a -> hours_to_minutes   a b
  static member        ($) (_: D3, b: int<seconds>) = fun a -> minutes_to_seconds a b

let inline (+) a b :'t = (Unchecked.defaultof<Sum> $ b) a

which is a bit more scalable, so I would consider using it in production code.

Problem

I want to be able to do this: ``` let duration = 1<hours> + 2<minutes> + 3<seconds> ``` with the following types and functions (and possibly more units of measure): ``` type [<Measure>] seconds type [<Measure>] minutes type [<Measure>] hours let seconds_per_minute = 60<seconds> / 1<minutes> let minutes_per_hour = 60<minutes> / 1<hours> let minutes_to_seconds minutes seconds = minutes * seconds_per_minute + seconds let hours_to_minutes hours minutes = hours * minutes_per_hour + minutes ``` So basically "hours_to_minutes" should be used for adding hours and minutes, and "minutes_to_seconds" should be used for adding minutes and seconds when I type it in like above. Is this possible to do in F#?

Original source