United Kingdom: +44 (0)208 088 8978

More powerful units of measure with FSharp.UMX

In this week's blog post, Matt takes a look at the FSharp.UMX library and shows how it extends F#'s core units of measure support.

We're hiring Software Developers

Click here to find out more

Units of measure: classic use

F# allows you to associate units of measure to numeric values. This can help you avoid errors such as dividing instead of multiplying by a conversion factor, and avoid the awkward situation where you say someone is 30 cms tall:

let heightInCms : float<cm> =
    let inches = imperialHeight.Feet * inchesPerFt + imperialHeight.Inches
    (float inches) / cmsPerInch // The unit of measure 'cm' does not match the unit of measure 'inch/cm'

Distinguishing between ID types

Although perhaps not what they were originally intended for, you can use units of measure to get the compiler to tell you if you accidentally use an ID for the wrong type of entity:

[<Measure>] type userId
type User = {
    Id: int<userId>
    Name: string
}

let username (userId: int<userId>) =
    users
    |> List.find (fun u -> u.Id = userId)
    |> (fun u -> u.Name)

[<Measure>] type chatId
type Chat = {
    Id: int<chatId>
    ParticipantOneId: int<userId>
    ParticipantTwoId: int<userId>
}

let chatTitle chat =
    let nameOne = username chat.ParticipantOneId
    let nameTwo = username chat.Id // Expecting a 'int<userId>' but given a 'int<chatId>'.
    $"Chat between {nameOne} and {nameTwo}"

This is a very lightweight way to protect against this type of error but, out of the box, unfortunately only works for numeric primitive types such as ints. This means that if you use a non-numeric type for IDs, such as Guids, you can't benefit from this very useful technique using only standard units of measure support.

type UserWithGuid = {
    Id: Guid<userId> // The non-generic type 'System.Guid' does not expect any type arguments...
    Name: string
}

FSharp.UMX

FSharp.UMX is a library that allows you to associate units of measure to non-numeric primitive types. This allows you to distinguish between ID types where the ID is not a numeric type, for example when IDs are Guids:

#r "nuget: FSharp.UMX"
open FSharp.UMX

type UserUmx = {
    Id: Guid<userId>
    Name: string
}

let user = {
    Id = % (Guid.NewGuid ())
    Name = ""
}
// Compiles successfully.

Thanks to the amazing contributors who made this possible!