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
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
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
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#.
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.
Top comments (0)