Single-case discriminated unions are a great way to avoid primitive obsession.
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.
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
value
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):
[<NoComparison>]
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
.