F#-intro in 3 minutes: Playing cards and discriminated union types
vain0x
Posted on February 13, 2020
I have written a short code to tell someone why I love F#. Describes some of the language features by writing a part of card game program as an example.
[1/4] Exhaustive pattern matching
I define Suit
as a discriminated union type. Same as enum types in C# here.
type Suit =
| Spade
| Clover
| Heart
| Diamond
For instance, I define a function to get the name of suits.
let suitToName suit =
match suit with
| Spade -> "Spade"
| Clover -> "Clover"
| Heart -> "Heart"
| Diamond -> "Diamond"
match...with
is a syntax for branching like switch
in C#. Unlike switch
statements, a match
expression gives a compile-time warning for you unless the patterns cover all cases.
The function works like this.
printfn "%s" (suitToName Spade) //=> "Spade"
And then let's see a compile-time warning. Comment the Diamond
case out.
let suitToName suit =
match suit with
| Spade -> "Spade"
| Clover -> "Clover"
| Heart -> "Heart"
// | Diamond -> "Diamond"
The compiler now emits a warning in a console or on an editor.
warning FS0025:
Incomplete pattern matches on this expression.
For example, the value 'Diamond' may indicate a case not covered by the pattern(s).
[2/4] Discriminated unions with fields
I also define Card
as a discriminated union type. Each case of discriminated union types can have fields.
type Card =
| NormalCard of suit:Suit * rank:int
| Joker
Field definitions follow of
and are separated by *
s.
You can tell that the code above almost literally describes the following facts.
- A card is either a normal card or Joker.
- A normal card has a suit and a rank.
- Joker doesn't have any property such as suit or rank.
A case with fields can new up with a constructor.
let heart3 = NormalCard (Heart, 3)
For instance, I define a function to get the name of cards. Same as the above example, make a branch and implement each case.
let cardToName card =
match card with
| Joker ->
"Joker"
| NormalCard (suit, rank) ->
let suitName = suitToName suit
let rankName = string rank
rankName + " of " + suitName
Output examples:
printfn "%s" (cardToName Joker) //=> Joker
printfn "%s" (cardToName heart3) //=> 3 of Heart
There are a large number of good situations where discriminated union types work fine.
type HttpResponse =
| OkWithText of text:string
| OkWithJson of json:obj
| Redirect of uri:string * temporary:bool
| InternalError of ex:exn
type BinaryTree<'T> =
| Node of left:BinaryTree<'T> * right:BinaryTree<'T>
| Leaf of value:'T
type Contact =
| ContactWithMail
of address:string
| ContactWithDelivery
of zipCode:string * address:string * recipient:string
[3/4] Type inference
By the way, the functions above compile even if I didn't write any parameter/result types explicitly. However, F# isn't a dynamic language -- type errors are reported at compile-time if any.
printfn "%s" (cardToName "Joker")
// ^^^^^^^ the constant Joker is correct here
Note that public APIs should be explicitly typed for readability, though.
[4/4] Wrapping up
In a nutshell, brevity is why I love F#. Could you image how much codes you need to write to define discriminated union types by using abstract classes and inheritance?
Of cause that's not all of the reason... however, it's time to end. I'm glad if you get interested in F#.
Appendix
On try.fsharp.org
, you can play with F# in the browser. This is the full code of the article.
And I made a quiz. Give it a try!
// Could you fix the code above to fix the name of aces?
// actual: 1 of Heart
// expected: ace of Heart
printfn "HeartA: %s" (cardToName (NormalCard (Heart, 1)))
// Hint: instead of the `string` function...
let rankToName rank =
match rank with
| 1 -> "I'm an ace."
| _ -> "I'm NOT an ace.'"
P.S. This is a translation of my article written in Japanese published on 2020-02-09.
Posted on February 13, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.