100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Active Patterns

Learn how F#'s active patterns let you define custom, named ways to pattern-match against values, from simple classification to parsing and matching against external data.

Functional TechniquesAdvanced10 min readJul 10, 2026
Analogies

Why Active Patterns Exist

Ordinary pattern matching in F# works directly against the shape of a value — the cases of a discriminated union, the fields of a tuple, a literal. Active patterns let you extend this same match syntax to values whose 'shape' isn't structurally obvious, by defining a function whose job is to classify or decompose the value into named cases. Instead of writing if isEven n then ... else ... and having to remember what isEven means at every call site, an active pattern like (|Even|Odd|) lets you write match n with | Even -> ... | Odd -> ..., so the classification logic is written once and the call site reads exactly like matching against a real discriminated union case, even though int has no such cases built in.

🏏

Cricket analogy: A scorer who invents shorthand categories like 'boundary' or 'dot ball' to describe deliveries, even though the rulebook only defines 'runs scored,' is creating a custom classification the way an active pattern like (|Even|Odd|) creates named cases for a type that has none built in.

Single-Case and Total Active Patterns

A total active pattern covers every possible input and returns one of a fixed set of named cases — let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd is total because every integer is either Even or Odd, with nothing left over. A single-case active pattern, written (|Name|), is often used not for classification but for decomposition or transformation: let (|Rectangle|) (w, h) = (w, h, w * h) lets you pattern-match a tuple while simultaneously computing a derived value, so matching against Rectangle (w, h, area) extracts width, height, and the computed area in one step, right inside the match arm.

🏏

Cricket analogy: A pitch report that always classifies conditions as either 'seaming' or 'spinning,' with no third option ever possible, mirrors how (|Even|Odd|) is total — every input falls into exactly one of the defined cases.

fsharp
// Total active pattern: covers every possible int
let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd

let describe n =
    match n with
    | Even -> sprintf "%d is even" n
    | Odd  -> sprintf "%d is odd" n

// Single-case active pattern: decomposes/transforms while matching
let (|Rectangle|) (w, h) = (w, h, w * h)

match (3, 4) with
| Rectangle (w, h, area) -> printfn "%dx%d = %d" w h area   // "3x4 = 12"

// Partial active pattern: may or may not match, returns an option
let (|Vowel|_|) (c: char) =
    if "aeiouAEIOU".Contains(c) then Some c else None

let classify c =
    match c with
    | Vowel v -> sprintf "%c is a vowel" v
    | _ -> sprintf "%c is a consonant" c

// Parameterized partial active pattern
let (|DivisibleBy|_|) divisor n =
    if n % divisor = 0 then Some () else None

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

Partial Active Patterns

Not every classification can be total — matching depends on runtime input like a regex or an external lookup, where 'no match' is a legitimate outcome rather than a bug. A partial active pattern is written (|Name|_|) and returns an option: let (|Vowel|_|) (c: char) = if "aeiouAEIOU".Contains(c) then Some c else None. Inside a match, | Vowel v -> ... only fires when the pattern returns Some, and F# requires a wildcard | _ -> ... arm afterward because the compiler can't prove a partial pattern is exhaustive. This is the mechanism behind libraries that let you pattern-match HTTP requests, regex captures, or JSON shapes as if they were ordinary discriminated union cases.

🏏

Cricket analogy: A DRS 'inconclusive' verdict, where the technology sometimes simply can't determine an outcome and play reverts to the on-field call, mirrors how a partial active pattern returns None when there's no clean match, requiring a fallback case.

Parameterized Active Patterns

Active patterns can also take extra arguments before the value being matched, letting you parameterize the classification itself. let (|DivisibleBy|_|) divisor n = if n % divisor = 0 then Some () else None takes divisor as a normal parameter and n as the value being matched; inside a match, you supply the extra argument directly in the pattern: | DivisibleBy 3 () -> "Fizz". This is exactly how a classic FizzBuzz can be written entirely through pattern matching, with DivisibleBy 15, DivisibleBy 3, and DivisibleBy 5 tried in order as ordinary match arms, reading far more declaratively than a chain of if/elif checks against n % 3 = 0 and n % 5 = 0.

🏏

Cricket analogy: A custom stat query like 'wickets taken against left-handed batsmen' takes a parameter (left-handed) before applying the classification to a bowler's record, mirrors how DivisibleBy takes divisor as a parameter before matching against n.

Active patterns are, under the hood, ordinary functions — (|Even|Odd|) compiles down to a function returning a special Choice-like type, and (|Vowel|_|) compiles down to a function returning option. This means they're called fresh on every match attempt with no caching, so an active pattern that does expensive work (a database lookup, a regex compile) inside its body will redo that work every time it's evaluated in a match arm.

F# limits total active patterns like (|A|B|C|...|) to a maximum of 7 named cases; beyond that, you'll need to restructure the classification (often into a real discriminated union) rather than push more cases into a single active pattern. Also, chaining many active patterns into one large match can obscure control flow just as badly as a wall of if/elif, so use them for genuinely reusable classifications, not as a one-off replacement for a simple boolean check.

  • Active patterns extend F#'s match syntax to any type by defining custom functions that classify or decompose values into named cases.
  • A total active pattern, written (|A|B|), covers every possible input; a single-case pattern (|Name|) is often used for decomposition or transformation.
  • A partial active pattern, written (|Name|_|), returns an option and is used when 'no match' is a legitimate outcome.
  • Match expressions using partial active patterns require a wildcard | _ -> arm because the compiler can't prove exhaustiveness.
  • Parameterized active patterns take extra arguments before the matched value, letting you customize the classification per use.
  • Active patterns are ordinary functions under the hood, re-evaluated on every match attempt with no automatic caching.
  • Total active patterns are capped at 7 named cases; beyond that, prefer a real discriminated union.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#FStudyNotes#ActivePatterns#Active#Patterns#Exist#Single#StudyNotes#SkillVeris#ExamPrep