Understanding Discriminated Unions from Enums

Wednesday, February 17, 2016

When I started learning F# I had a hard time grokking how and when to use a Discriminated Union (Sum type). I had experience in several paradigms and languages, but had never seen or used this data construct. In this article we'll start with an Enum and then work our way into an example use case with a Discriminated Union

enum HttpMethod
{
    Get, Post
}

An Enum is basically a constrained type where an instance of the enum must be one of the specified values. For an instance to be of the type HttpMethod in this case it has to be either Get or Post.

One way to write the same idea in F# is to create a Discriminated Union

type HttpMethod = 
    | Get 
    | Post

At first glance these two appear to be equivalent and they mostly are, but the enum at this point is tapped out; we can't get any more information in or out of it. The Discriminated Union on the other hand can be much more powerful.

If we wanted to know the associated url in the Enum approach we would need an additional parent type

class HttpRequest
{
    public HttpMethod HttpMethod { get; set; }
    public string Url { get; set; }
}

With the DU we can define that each case also has a string attached which (in this case anyway) is kind of like the inverse of the Enum approach.

type HttpRequest =
    | Get of string
    | Post of string

Of course, if we were really going to handle a Post request we'd need to consider the content of the post request which wouldn't be needed in the Get request. With the Enum approach we could add another property to the HttpRequest class, but that seems a bit like a Liskov violation smell to me. (the content will never be used for some cases in the Enum).

class HttpRequest
{
    public HttpMethod HttpMethod { get; set; }
    public string Url { get; set; }
    
    //only used if HttpMethod is Post
    public string Content { get; set; }
}

This is where the Discriminated Union shines. It's designed specifically for this purpose and is trivial to add additional information to only the Post branch.

type HttpRequest = 
    | Get of string
    | Post of string * string

Now if the HttpRequest is a Get it will still have a string, but if it is a Post it will have a tuple of string and string. In this approach it is not possible to create an HttpRequest that is a Get and contains content.

We can now easily create and use values of our HttpRequest type

let handleRequest request =
    match request with
    | Get url -> printfn "url = %s" url
    | Post (url, content) -> 
        printfn "url = %s and content = %s" url content

let get = Get "https://www.google.com"
handleRequest get
//url = https://www.google.com

let post = Post ("https://www.google.com", "Hello World")
handleRequest post
//url = https://www.google.com and content = Hello World

If we create a true Enum in F# (which you would probably rarely do unless interoping with the other .Net languages) we can see that an Enum is like a Discriminated Union where every option has the same type associated with it and that type must be a compile time constant int.

type HttpRequestEnum = 
    | Get = 0
    | Post = 1