Extending a 3rd-party API with F# units of measure
F# units of measure are nice, but what if I'm using a 3rd-party library which doesn't support UOM? I don't want to re-implement the library, but it would be nice to add the extra safety afforded by UOM. Can I somehow annotate the APIs of the 3rd party library with units?
How to do it
Say you have some unit-free external library like this, which you can't modify:
type RectLib = static member Perimeter(w:int, h:int) = 2*w + 2*h static member Area(w:int, h:int) = w * h
Wouldn't it be great if we could enforce in our code that
h have the same units? Or that the result of
Area has squared units compared to the result of
You can do exactly that, by defining extensions like so:
module UnitExtensions = open LanguagePrimitives type RectLib with static member Perimeter(w:int<'u>, h:int<'u>) = RectLib.Perimeter(int w, int h) |> Int32WithMeasure<'u> static member Area(w:int<'u>, h:int<'u>) = RectLib.Area(int w, int h) |> Int32WithMeasure<'u^2>
Now units are enforced at compile time by the type system:
open UnitExtensions [<Measure>] type ft [<Measure>] type m RectLib.Perimeter(1<m>, 2<ft>) // error - <m> doesn't match <ft> RectLib.Perimeter(3<m>, 4<m>) // ok RectLib.Area(5<m>, 6<m>) + RectLib.Perimeter(7<m>, 8<m>) // error - <m> doesn't match <m^2> RectLib.Area(9<ft>, 10<ft>) + RectLib.Area(11<m>, 12<m>) // error - <ft^2> doesn't match <m^2> RectLib.Area(9<ft>, 10<ft>) + RectLib.Area(11<ft>, 12<ft>) // ok
Does this wrapper introduce a lot of overhead? It looks like a lot of conversions to and from values with and without units. I don't want to wreck the perf of my code by adding this.
F# units of measure are carried along and enforced throughout the compilation, but they are completely erased when it come to codegen. The unit information simply doesn't exist in the emitted assembly. So in general they don't introduce any runtime performance cost.
Creating wrapper methods like above does potentially introduce a very small amount of overhead. At worst you introduce a single thin method call (the wrapper method does nothing but call the "real" method with the same args), and at best (with optimizations on and when various conditions are met) things get fully inlined and there is no overhead whatsoever.
Using units from the beginning
Note that if we were to have written the original library with units support from the start, we could do it cleanly and simply like this:
type RectLib = static member Perimeter(w:int<'u>, h:int<'u>) = 2*w + 2*h static member Area(w:int<'u>, h:int<'u>) = w * h
And the rest would have followed automatically.