29 March 2026

Deck of Cards

In my last post, I used the analogy of creating a deck of cards to illustrate the relationship between discriminated unions and pattern matching. That example was taken from Chris Smith’s “Programming in F# 3.0”. I know that the approach taken there was more to serve as a teaching example but a few things about the way we constructed the PlayingCard type bothered me:

> 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

First off, I didn’t like how the union types for Ace and the face cards differed in type from the numeral cards. The former consisted of just the Suit while the latter was constructed using a tuple of int and Suit. Second, there was nothing preventing someone from creating a card such as:

> let card = ValueCard (723, Spade);;
val card: PlayingCard = ValueCard (723, Spade)

I think if I were to do this again, I would do it slightly differently. I would keep the original type to define the Suit:

type Suit =
  | Heart
  | Diamond
  | Spade
  | Club

But I would create a second discriminated union to define the Rank:

> type Rank =
-     | Ace
-     | King
-     | Queen
-     | Jack
-     | Ten
-     | Nine
-     | Eight
-     | Seven
-     | Six
-     | Five
-     | Four
-     | Three
-     | Two;;
type Rank =
  | Ace
  | King
  | Queen
  | Jack
  | Ten
  | Nine
  | Eight
  | Seven
  | Six
  | Five
  | Four
  | Three
  | Two

This enforces correctness at the type level obivating the need for any runtime checks.

Now we can create our PlayingCard record:

> type PlayingCard = { Rank: Rank; Suit: Suit };;
type PlayingCard =
  {
    Rank: Rank
    Suit: Suit
  }

With the assistance of some “helper lists”, we can create the list for our deckOfCards:

> let ranks =              
-     [
-         Ace; King; Queen; Jack;
-         Ten; Nine; Eight; Seven;
-         Six; Five; Four; Three; Two
-     ];;
val ranks: Rank list =
  [Ace; King; Queen; Jack; Ten; Nine; Eight; Seven; Six; Five; Four; Three;
   Two]

> let suits = [ Heart; Diamond; Spade; Club ];;
val suits: Suit list = [Heart; Diamond; Spade; Club]

> let deckOfCards =
-     let toCard (rank, suit) = { Rank = rank; Suit = suit }
-     List.allPairs ranks suits
-     |> List.map toCard;;
val deckOfCards: PlayingCard list =
  [{ Rank = Ace
     Suit = Heart }; { Rank = Ace
                       Suit = Diamond }; { Rank = Ace
                                           Suit = Spade }; { Rank = Ace
                                                             Suit = Club };
   { Rank = King
     Suit = Heart }; { Rank = King
                       Suit = Diamond }; { Rank = King
                                           Suit = Spade }; { Rank = King
                                                             Suit = Club };
   { Rank = Queen
     Suit = Heart }; { Rank = Queen
                       Suit = Diamond }; { Rank = Queen
                                           Suit = Spade }; { Rank = Queen
                                                             Suit = Club };
   { Rank = Jack
     Suit = Heart }; { Rank = Jack
                       Suit = Diamond }; { Rank = Jack
                                           Suit = Spade }; { Rank = Jack
                                                             Suit = Club };
...

Not only is this approach more strict, there really is no need for pattern matching to “consume” a card:

> let describe card = sprintf $"{card.Rank} of {card.Suit}";;
val describe: card: PlayingCard -> string

> describe deckOfCards[22];;
val it: string = "Nine of Spade"

And if you look closely, we also eliminated the nested for loops in the deckOfCards function with some List methods. I think I like this way much better!