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!