DEV Community

vain0x
vain0x

Posted on

F#-intro in 3 minutes: Playing cards and discriminated union types

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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).
Enter fullscreen mode Exit fullscreen mode

[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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Output examples:

printfn "%s" (cardToName Joker) //=> Joker
printfn "%s" (cardToName heart3) //=> 3 of Heart
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

[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
Enter fullscreen mode Exit fullscreen mode

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.'"
Enter fullscreen mode Exit fullscreen mode

P.S. This is a translation of my article written in Japanese published on 2020-02-09.

Top comments (0)