Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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#?

like image 806
user3323923 Avatar asked Feb 18 '14 18:02

user3323923


2 Answers

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.

like image 110
Gus Avatar answered Oct 30 '22 01:10

Gus


This isn't possible directly within F#. There isn't a way to have the "auto-conversion" succeed directly without specifying the conversion for the types. You would have to explicitly call your conversion functions (seconds_per_minute, etc).

However, Phil Trelford demonstrated a mechanism by which you could create runtime classes which do support this, though with slightly different syntax. Using his types, you could write:

let duration = 1.0 * SI.hours + 2.0 * SI.minutes + 3.0 * SI.seconds
like image 29
Reed Copsey Avatar answered Oct 30 '22 00:10

Reed Copsey