Skip to content
AgileLogics
6 min read

Active Patterns

Prerequisites: Pattern matching in depth

Active patterns let you define your own patterns — functions that match can call to classify or take apart a value. They're one of F#'s most distinctive features.

Complete active patterns

A pattern with several cases, written in "banana clips" (| ... |), splits every input into one of the cases:

let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd

let (|Negative|Zero|Positive|) n =
    if n < 0 then Negative elif n = 0 then Zero else Positive

let describe n =
    match n with
    | Zero -> "zero"
    | Negative & Even -> "negative even"
    | Negative -> "negative odd"
    | Positive & Even -> "positive even"
    | Positive -> "positive odd"

for n in [-4; -3; 0; 7; 10] do
    printfn "%d: %s" n (describe n)
-4: negative even
-3: negative odd
0: zero
7: positive odd
10: positive even

& combines patterns: the value must match both.

Partial active patterns

A pattern that may not match ends with |_| and returns an option:

let (|Int|_|) (s: string) =
    match System.Int32.TryParse s with
    | true, n -> Some n
    | _ -> None

let (|Bool|_|) (s: string) =
    match s.ToLower() with
    | "true" | "yes" -> Some true
    | "false" | "no" -> Some false
    | _ -> None

let parse input =
    match input with
    | Int n -> $"integer {n}"
    | Bool b -> $"boolean %b{b}"
    | other -> $"text '{other}'"

["42"; "Yes"; "hello"] |> List.iter (parse >> printfn "%s")
integer 42
boolean true
text 'hello'

The messy parsing code is hidden in the patterns, and the match reads like a description of the input format.

Parameterized patterns and regexes

Active patterns can take extra arguments before the value. A regex pattern is the classic example:

open System.Text.RegularExpressions

let (|Regex|_|) pattern input =
    let m = Regex.Match(input, pattern)
    if m.Success then Some [ for g in m.Groups |> Seq.skip 1 -> g.Value ]
    else None

let parseCommand line =
    match line with
    | Regex @"^move (\d+) (\d+)$" [ x; y ] -> $"move to {x},{y}"
    | Regex @"^say (.+)$" [ text ] -> $"say \"{text}\""
    | Regex @"^quit$" [] -> "quit"
    | _ -> "unknown command"

[ "move 3 4"; "say hello world"; "quit"; "jump" ]
|> List.iter (parseCommand >> printfn "%s")
move to 3,4
say "hello world"
quit
unknown command

Single-case patterns

A single-case active pattern transforms a value — handy for normalizing input right in a parameter:

let (|Trimmed|) (s: string) = s.Trim().ToLower()

let isYes (Trimmed answer) = answer = "yes" || answer = "y"

printfn "%b %b %b" (isYes "  YES ") (isYes "y") (isYes "nope")
true true false

Practice

Write a partial active pattern (|DivisibleBy|_|) d n that matches when n is divisible by d, and use it for FizzBuzz from 1 to 15.

Show solution
let (|DivisibleBy|_|) d n = if n % d = 0 then Some () else None

let fizzBuzz n =
    match n with
    | DivisibleBy 15 -> "FizzBuzz"
    | DivisibleBy 3 -> "Fizz"
    | DivisibleBy 5 -> "Buzz"
    | _ -> string n

[1 .. 15] |> List.map fizzBuzz |> String.concat " " |> printfn "%s"
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz

A partial pattern that carries no value returns Some (). (Since F# 9 it may also simply return true or false.)