F# tips weekly #1: Single case DU

F# tips weekly #1: Single case DU

Single-case discriminated unions are a great way to avoid primitive obsession.

Primitive obsession is an anti-pattern where primitive types are used too much in places where a custom type would be a better fit. By using custom types or wrapper types, we can catch more potential bugs.

We can create a new type that simply wraps the value of another (typically primitive) type. In F#, this is a simple one-liner:

type ItemId = ItemId of int

By using ItemId instead of int, we can ensure that it is used only in places where it is explicitly requested. Also, using this type is self-documenting.

For example, this function:

let getItem (x: ItemId) =

can't be called with a different type (let's say CustomerId) - preventing mistakes that could happen if we used just int.

When we need to access the value inside the type, we can take advantage of the fact that pattern matching can be used on the left-hand side of let:

let itemId = ItemId 42
let (ItemId value) = itemId

And in function parameters:

let getItem (ItemId value) =

And also in lambda functions:

[ ItemId 1; ItemId 2; ItemId 3 ] |> List.map (fun (ItemId value) -> value)

But sometimes using pattern matching can be cumbersome because we must create a new binding. We can improve this by defining a helper member on the DU type:

type ItemId = ItemId of int
with member this.Value = let (ItemId value) = this in value

Then we can use itemId.Value.

The in keyword is shorthand syntax for writing let binding and following expression in one line. Learn more on fsharpforfunandprofit.
member this.Value = 
    let (ItemId value) = this

Another advantage is that we can restrict the usage of our type. For example, we can disallow comparing values (because for IDs it doesn't make sense):

type ItemId = ItemId of int

(ItemId 1) < (ItemId 2)

This will generate an error: The type 'ItemId' does not support the 'comparison' constraint because it has the 'NoComparison' attribute1.