28 March 2026

Discriminated Unions and Pattern Matching in F#

I’m starting to kick the tires on discriminated unions and pattern matching as I continue to learn about F#. While not exactly the same thing, they are kind of like C#’s enumeration types where a type can have only one possible value. That value is called a union case. Using playing cards as an example, we can define the type Suit:

> type Suit =
-     | Heart
-     | Diamond
-     | Spade
-     | Club;;
type Suit =
  | Heart
  | Diamond
  | Spade
  | Club

> Diamond;;
val it: Suit = Diamond

Here, we can see that Diamond is of type Suit.

Discriminated unions are used to define what is sometimes known as an “or” relationship which, in our example, means that the suit of a playing card can be a Heart or a Diamond or a Spade or a Club. This is in contrast to F# Records which help define an “and” relationship.

We can then define the type PlayingCard as being either an Ace, a face/court card (King, Queen or Jack) or a numeral card (2 through 10, inclusive) using another discriminated union:

> type PlayingCard =
-     | Ace of Suit
-     | King of Suit
-     | Queen of Suit
-     | Jack of Suit
-     | ValueCard of int * Suit;;
type PlayingCard =
  | Ace of Suit
  | King of Suit
  | Queen of Suit
  | Jack of Suit
  | ValueCard of int * Suit

> Ace;;
val it: Item: Suit -> PlayingCard

> ValueCard;;
val it: Item1: int * Item2: Suit -> PlayingCard

In defining the type PlayingCard we have chosen to associate data with each union case. This is optional. The data we associate with the union case can be a simple type (e.g. int, string), a tuple, a record or even another discriminated union. In our case, we’ve associated the discriminated union Suit to the union cases Ace, King, Queen and Jack. For the union case ValueCard, we’ve associcated a tuple consisting of an int and a Suit.

Note how F# treats these union cases as a function when we examine their type signatures. Ace, when provided a parameter of type Suit, will return a PlayingCard. ValueCard is also a function that returns a PlayingCard but it takes a tuple made up of an int and Suit for its parameter. This is very important because understanding the type signature not only helps us in constructing a card but, as we’ll see later, also guides us in defining the correct patterns when it comes to matching a card. Knowing this, we can define a few individual cards:

> let card0 = Ace Spade;;
val card0: PlayingCard = Ace Spade

> let card1 = King Spade;;
val card1: PlayingCard = King Spade

> let card4 = ValueCard (2, Spade);;
val card4: PlayingCard = ValueCard (2, Spade)

> let card18 = ValueCard (3, Club);; 
val card18: PlayingCard = ValueCard (3, Club)

(Note that when defining a card that belongs to the ValueCard union case, parentheses are required because we have to pass a tuple. The parentheses are optional if we are defining a card with the union case Ace, King, Queen or Jack. We can include parenetheses for readability of we desire like in the example below.)

Armed with these discriminated unions, we can now create a deckOfCards:

> let deckOfCards =
-     [
-         for suit in [ Spade; Club; Heart; Diamond ] do
-             yield Ace(suit)
-             yield King(suit)
-             yield Queen(suit)
-             yield Jack(suit)
-             for value in 2 .. 10 do
-                 yield ValueCard(value, suit)
-     ];;
val deckOfCards: PlayingCard list =
  [Ace Spade; King Spade; Queen Spade; Jack Spade; ValueCard (2, Spade);
   ValueCard (3, Spade); ValueCard (4, Spade); ValueCard (5, Spade);
   ValueCard (6, Spade); ValueCard (7, Spade); ValueCard (8, Spade);
   ValueCard (9, Spade); ValueCard (10, Spade); Ace Club; King Club;
   Queen Club; Jack Club; ValueCard (2, Club); ValueCard (3, Club);
   ValueCard (4, Club); ValueCard (5, Club); ValueCard (6, Club);
   ValueCard (7, Club); ValueCard (8, Club); ValueCard (9, Club);
   ValueCard (10, Club); Ace Heart; King Heart; Queen Heart; Jack Heart;
   ValueCard (2, Heart); ValueCard (3, Heart); ValueCard (4, Heart);
   ValueCard (5, Heart); ValueCard (6, Heart); ValueCard (7, Heart);
   ValueCard (8, Heart); ValueCard (9, Heart); ValueCard (10, Heart);
   Ace Diamond; King Diamond; Queen Diamond; Jack Diamond;
   ValueCard (2, Diamond); ValueCard (3, Diamond); ValueCard (4, Diamond);
   ValueCard (5, Diamond); ValueCard (6, Diamond); ValueCard (7, Diamond);
   ValueCard (8, Diamond); ValueCard (9, Diamond); ValueCard (10, Diamond)]

Now that we have our deckOfCards, we can see how discriminated unions and pattern matching can work together by creating a describe function which will take a parameter card of type PlayingCard and return a string that tells us what type of card we have. Let’s take a minute to quickly revisit the type signatures of the union cases that make up the PlayingCard type:

> Ace;;
val it: Item: Suit -> PlayingCard

> ValueCard;;
val it: Item1: int * Item2: Suit -> PlayingCard

Using Ace as an example, we can mentally think of how we used its signature to create individual cards above:

Ace : Suit -> PlayingCard

Using the same logic, we can “deconstruct” the card in a way to be used in pattern matching. Let’s start with just one specific rule and one wildcard (to silence distracting compiler warnings):

> let describe card =
-     match card with
-     | Ace suit -> $"Ace of %A{suit}"  // note the "Ace suit" pattern
-     | _ -> "another card";;
val describe: card: PlayingCard -> string

> describe deckOfCards[0];;           
val it: string = "Ace of Spade"

> describe deckOfCards[1];;
val it: string = "another card"

Using the same format, we can build up our describe function to include the face cards:

> let describe card =
-     match card with
-     | Ace suit -> $"Ace of %A{suit}"
-     | King suit -> $"King of %A{suit}"
-     | Queen suit -> $"Queen of %A{suit}"
-     | Jack suit -> $"Jack of %A{suit}"
-     | _ -> "another card";;
val describe: card: PlayingCard -> string

> describe deckOfCards[3];;
val it: string = "Jack of Spade"

We can now match for ValueCards but with a slightly different pattern. Again, going back to the ValueCard type signature, we see that it takes a tuple of an int and Suit. Another way to think about this is:

ValueCard : (int * Suit) -> PlayingCard

Therefore, we can augment our describe function with a pattern to map to ValueCards:

> let describe card =
-     match card with
-     | Ace suit -> $"Ace of %A{suit}"
-     | King suit -> $"King of %A{suit}"
-     | Queen suit -> $"Queen of %A{suit}"
-     | Jack suit -> $"Jack of %A{suit}"
-     | ValueCard (value, suit) -> $"{value} of %A{suit}";; // note the tuple
val describe: card: PlayingCard -> string

> describe deckOfCards[4];;
val it: string = "2 of Spade"

> describe deckOfCards[18];;
val it: string = "3 of Club"

Note that because we have now accounted for all possible union cases, we no longer require the wildcard pattern.

Like all of my prior posts on F#, this is in now way meant to be an exhaustive or comprehensive treatise on discriminated unions or pattern matching. Rather, it’s a brief summary of my limited understanding of how they work to hopefully help improve my comprehension. As usual, comments, suggestions or feedback are greatly appreciated!