Eliminating Bugs with Single Case Discriminated Unions

While reviewing the bugs that I've written in F# over the course of my most recent project I've found that the only recurring bug I had was passing arguments of the same type in the wrong order.

Basic Example

Here's a contrived and simple example of what I mean.

let assignUserToCustomer userId customerId =  
    use command = new DbCommand()
    command.Execute(userId, customerId)
    Some()

assignUserToCustomer customerId userId  

In this case userId and customerId are both ints so the fact that I've swapped them goes unnoticed by the compiler. The signature for assignUserToCustomer is int -> int -> unit option so while it looks obvious in this case that I've swapped the order, the compiler doesn't know.

In languages without algebraic data types, resolving this problem typically leads to lots of primitive wrapper classes, each with loads of boilerplate to override equality and other such things.

In languages like F# and Haskell that have algebraic data types this is where a Single Case Discriminated Union (or Sum type) can eliminate this whole class of errors from happening.

What is a Single Case Discriminated Union?

Here's an example

type CustomerId = CustomerId of int

//create a CustomerId
let customer = CustomerId 123  
//val customer : CustomerId = CustomerId 123

//get the int value of the CustomerId
let (CustomerId id) = customer  
// val id : int = 123

It's just like any other Discriminated Union except that it only has one case. Note that it is common with this pattern for the case and the type to have the same name, the compiler is smart enough to know whether you're creating or destructuring.

Rewriting our error prone code

The first thing to do is create the Single Case Discriminated Unions that we need.

type CustomerId = CustomerId of int  
type UserId = UserId of int

let customer = CustomerId 2  
let user = UserId 123  

Once we have created the types and we have values for the types (customer and user) we can alter our initial function to automatically destructure the types to ints so that we can maintain the same database calling code.

let assignUserToCustomer (UserId userId) (CustomerId customerId) =  
    use command = new DbCommand()
    command.Execute(userId, customerId)
    Some()

assignUserToCustomer user customer  

If you look at the type of userId or customerId you'll see that they're both ints which is what our data call expects. By defining the argument as (UserId userId) we get to work with the underlying int value immediately without the ceremony of having a full pattern match on each Single Case DU.

Perhaps even more important is that if you look at the signature for the function though it's now UserId -> CustomerId -> unit option rather than int -> int -> unit option. Our code will no longer compile if we attempt to reorder the arguments.

Related Posts

Proudly published with Ghost