Pattern Matching in Depth
Prerequisites: Discriminated unions
Patterns can be combined and nested, and they work in more places than
match.
Nested patterns
type Contact =
| Email of string
| Phone of countryCode: int * number: string
type Customer = { Name: string; Contact: Contact option }
let howToReach customer =
match customer with
| { Contact = Some (Email address) } -> $"email {address}"
| { Contact = Some (Phone (1, number)) } -> $"call {number} (US)"
| { Contact = Some (Phone (code, number)) } -> $"call +{code} {number}"
| { Name = name; Contact = None } -> $"no contact for {name}"
[ { Name = "Ada"; Contact = Some (Email "ada@example.com") }
{ Name = "Bob"; Contact = Some (Phone (1, "555-0100")) }
{ Name = "Chen"; Contact = Some (Phone (86, "10-1234")) }
{ Name = "Dee"; Contact = None } ]
|> List.iter (howToReach >> printfn "%s")
email ada@example.com
call 555-0100 (US)
call +86 10-1234
no contact for Dee
Patterns go as deep as your data: a record containing an option
containing a union containing a tuple containing a literal 1.
as, and &, and type tests
let describe (value: obj) =
match value with
| :? int as n when n > 100 -> $"big int {n}"
| :? int as n -> $"int {n}"
| :? string as s -> $"string of length {s.Length}"
| null -> "null"
| other -> $"something else: {other}"
printfn "%s" (describe 7)
printfn "%s" (describe 1000)
printfn "%s" (describe "hello")
printfn "%s" (describe null)
printfn "%s" (describe 2.5)
let (x, y) as point = (3, 4)
printfn "%A %d %d" point x y
int 7
big int 1000
string of length 5
null
something else: 2.5
(3, 4) 3 4
:? int as ntests the runtime type (useful withobjfrom .NET APIs).pattern as namebinds the whole matched value as well as its parts.p1 & p2matches only if both patterns match (mostly used with active patterns, later in this level).
Array and list patterns
let command args =
match args with
| [| "add"; a; b |] -> $"sum = {int a + int b}"
| [| "greet"; name |] -> $"hello, {name}"
| [| "help" |] | [||] -> "usage: add A B | greet NAME"
| _ -> "unknown command"
printfn "%s" (command [| "add"; "2"; "3" |])
printfn "%s" (command [| "greet"; "Ada" |])
printfn "%s" (command [||])
printfn "%s" (command [| "dance" |])
let rec pairs list =
match list with
| a :: b :: rest -> (a, b) :: pairs rest
| _ -> []
printfn "%A" (pairs [1; 2; 3; 4; 5])
sum = 5
hello, Ada
usage: add A B | greet NAME
unknown command
[(1, 2); (3, 4)]
Patterns in let and function parameters
Any pattern that always matches can be used in let and in parameters:
type Point = { X: int; Y: int }
let distanceSquared { X = x1; Y = y1 } { X = x2; Y = y2 } =
(x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)
let { X = px } = { X = 5; Y = 9 }
printfn "%d" (distanceSquared { X = 0; Y = 0 } { X = 3; Y = 4 })
printfn "%d" px
[ ("a", 1); ("b", 2) ] |> List.iter (fun (key, value) -> printfn "%s=%d" key value)
25
5
a=1
b=2
Tips
- Order cases from most specific to most general — F# picks the first match, and warns about rules that can never match.
- Prefer handling every union case explicitly over a final
_, so the compiler can tell you when a new case is added. - Keep guards (
when) simple; if a guard gets complicated, consider an active pattern.