Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
How to extend a 3rd-party API with F# units of measure

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?

Yes!

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 w and h have the same units? Or that the result of Area has squared units compared to the result of Perimeter?

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>

Results

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

Codegen

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.