4 min read

F#: Exploring Discriminated Unions

F# stands out as a language that offers an expressive way to model data via discriminate union. It represents a finite, well-defined set of choices. It is often the tool of choice for building up more complicated data structures.
F#: Exploring Discriminated Unions
Photo by Emmanuel Appiah / Unsplash

Introduction

One of the unique features that F# brings to the table is "discriminated unions."

This concept allows you to define custom data types representing various shapes and formats, enabling you to create flexible, type-safe, and concise code.

When I first saw a discriminated union inside an F# project, I told myself it was an enum. However, as I learn more about the F# language, enums are limited to fixed values. At the same time, discriminated unions can hold values of varying types and structures ( We'll discuss this more, sharing my experience and thoughts😁).

Okay, let's get started then.

What is a Discriminated Union?

At its core, a discriminated union is a type that represents a value that can be one of several distinct possibilities. These possibilities are defined using constructors, each of which can carry different data.

How to Define Discriminated Union?

See the sample syntax below to define a discriminate union in F#.

type type-name =
    | constructor-1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
    | constructor-2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]
    ...

As you can see, we can create a new discriminated union by using the type, | symbol, and the name of the identifier or constructor. Moreover, a constructor can carry zero or more fields.

Example of a Discriminated Union

Let's try to see an example below.

type DogBreed  = 
         | GermanShepherd
         | Pug of areYouInlove:bool
         | Labrador of age:int
         | Poodle of color:string
         | Bulldog of name:string * country:string

In the example above, we have introduced the DogBreed discriminated union featuring three constructors: GermanShepherd, Pug, Labrador, Poodle, and Bulldog.

Each constructor accommodates data relevant to its specific dog breed.

This elegant modeling ensures a transparent representation of distinct dog breeds within a single type. How's that? Pretty right?

Discriminated Union and Pattern Matching

The true power of discriminated unions surfaces when combined with pattern matching.

Pattern matching is a mechanism that allows for the deconstruction and handling of data contained within a discriminated union in a manner that is both succinct and type-safe.

Think of pattern matching as a turbocharged iteration of the switch statement in C#.

Example

Let's try to see an example.

module Dogs =
    
    type DogBreed  = 
         | GermanShepherd
         | Pug of areYouInlove:bool
         | Labrador of age:int
         | Poodle of color:string
         | Bulldog of name:string * country:string

    let describeDog dog =
        match dog with
            | GermanShepherd -> "Smart Dog"
            | Pug(areYouInlove) ->  $"Pug are the best and you are in love? {areYouInlove}"
            | Labrador(age) when age <= 2 -> "Adorable Labrador puppy!"
            | Labrador(_) -> "Loyal Labrador"
            | Poodle(color) -> sprintf "Elegant %s Poodle" color
            | Bulldog(name, country) -> sprintf "%s, the Bulldog from %s" name country
let playWithDescribeDogs () = 

    let pug =  Pug(true)
    let dogResult1 = describeDog pug
    printfn "%s" dogResult1

    let pugAgain = Pug false
    let dogResult2 = describeDog pugAgain
    printfn "%s" dogResult2

    let labrador = Labrador 2
    let dogResult3 = describeDog labrador
    printfn "%s" dogResult3

    let oldLabrador = Labrador 3
    let dogResult4 = describeDog oldLabrador 
    printfn "%s" dogResult4

    let poddle = Poodle "white"
    let dogResult5 = describeDog poddle 
    printfn "%s" dogResult5

    let bulldog = Bulldog("Bruno","Philippines")
    let dogResult6 = describeDog bulldog
    printfn "%s" dogResult6

playWithDescribeDogs()

In this example, we've created instances of different dog breeds using the constructors ( GermanShepherd, Pug, Labrador, Poddle,  and Bulldog)   defined in the DogBreed discriminated union.

We then invoke the describeDog function on each instance and print out the descriptions.

The pattern matching in the describeDog function determines the appropriate description for each breed instance.

Remember that F# pattern matching allows you to elegantly handle different cases and variations in your data structures, making your code both concise and expressive.

My Thoughts When I Started with Discriminated Union

Again, I was confused the first time I saw a discriminated union.

When I was looking for those constructors inside a discriminated union, and I couldn't find them (I was expecting if they were defined somewhere else).

However, I was scratching my head when I saw a field name with its type. What the heck is it?

Now, I'm pretty much seeing it every day.

All I can say is: "F# discriminated unions are a bunch of classes."

For everyone to understand, let me try to convert the DogBreed type into a C# equivalent.

    public class GermanShepherd 
    {
        
    }

    public class Pug
    {
        public bool AreYouInLove { get; set; }
    }
    public class Labrador
    {
        public int Age { get; set; }
    }
    public class Poodle
    {
        public string Color { get; set; }
    }
    public class Bulldog
    {
        public string Name { get; set; }
        public string Country { get; set; }
    }
    public class DogBreed
    {
        public GermanShepherd GermanShepherd { get;  }
        public Pug Pug { get;  }
        public Labrador Labrador { get; set; }
        public Poodle Poodle { get; set; }
        public Bulldog Bulldog { get; set; }

        public DogBreed(GermanShepherd germanShepherd)
        {
            this.GermanShepherd = germanShepherd;
        }

        public DogBreed(Pug pug)
        {
            this.Pug = pug;
        }

        public DogBreed(Labrador labrador)
        {
            this.Labrador = labrador;
        }

        public DogBreed(Poodle poodle)
        {
            this.Poodle = poodle;
        }
        public DogBreed(Bulldog bulldog)
        {
            this.Bulldog = bulldog;
        }
    }

This code snippet represents the F# discriminated union DogBreed as closely as possible in C#.

Each constructor for a specific breed is encapsulated within the nested classes GermanShepherd, Pug, Labrador, Poodle, and Bulldog.

The constructors create instances of these nested classes, which are then used to initialize the DogBreed instances.

Please note that while C# doesn't have native support for discriminated unions like F#, this approach aims to mimic the intent and behavior of the original F# example using C#'s object-oriented features.

Advantages of Discriminated Union

Let's try to see some of the advantages of discriminated union.

Enhanced Type Safety and Exhaustiveness

It provides increased type safety because each case requires handling during pattern matching, the risk of discovering runtime errors due to missed cases decreases considerably.

Innate Immutability

F# promotes immutability, and discriminated unions naturally adhere to this idea. A discriminated union's values are immutable by default, fostering clean, functional programming.

Conciseness and Expressiveness

Discriminated unions allow for the concise representation of complex data structures, resulting in more readable and manageable codebases. They do away with the need for complex if-else constructs or huge class hierarchies.

Conclusion

Discriminated unions exemplify the power of expressive data modeling in the world of functional programming. Moreover, they enable the depiction of complicated data structures with diverse forms and formats. Pattern matching ensures efficient and error-free handling.

Accept the adventure, play with possibilities, and experience the elegance of F# discriminated unions firsthand. Your codebase will become more structured, robust, and inherently linked with functional programming principles.

I hope you have enjoyed this article. Till next time, happy programming!