Skip to content
AgileLogics
6 min read

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 n tests the runtime type (useful with obj from .NET APIs).
  • pattern as name binds the whole matched value as well as its parts.
  • p1 & p2 matches 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.