jonnifyfsharp

Dive C1.05.3

Active patterns

Name a match case with your own logic, then use it like a built-in.

The patterns so far are the ones F# supplies — constants, lists, tuples. An active pattern lets a programmer add a case of their own: a small function, written in banana brackets (| … |), that the matcher calls and treats as a pattern. A complete active pattern such as (|Even|Odd|) sorts every input into one of its named cases; a partial one such as (|Int|_|) returns an option and may decline. In a match, the result reads like any other arm — | Even, | Int v — and that is the point.

C1.05.3 · part 1

Complete active patterns

A complete active pattern lists its cases in its name and returns one of them for every input. The definition below partitions an int into Even and Odd. The names inside the brackets become usable patterns — a match arm can then be written | Even -> ….

let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd
// val (|Even|Odd|) : int -> Choice<unit,unit>

match 7 with | Even -> "even" | Odd -> "odd"   // val it : string = "odd"
The inferred type is int -> Choice<unit,unit>. The case names Even and Odd act as patterns, not as values of a discriminated union — F# represents a two-way partition with the built-in Choice<_,_> type. Returning Even from the function body is the syntax for "this input falls in the first case".
(| … |)
banana brackets: the active-pattern definition syntax; the names between them are the cases.
complete
every input maps to exactly one named case; the match over it is exhaustive.
Choice<unit,unit>
how F# types a two-case complete active pattern — the partition, not a DU you declared.
C1.05.3 · part 2

Partial active patterns

Not every test partitions its input. A partial active pattern ends its name with |_| and returns an option: Some v to match and bind v, or None to decline and let the next arm try. The pattern below wraps Int32.TryParse, turning a string into an int only when it parses.

let (|Int|_|) (s: string) =
    match System.Int32.TryParse s with
    | true, v -> Some v
    | false, _ -> None
// val (|Int|_|) : string -> int option

match "42" with | Int v -> v | _ -> -1   // val it : int = 42
match "x"  with | Int v -> v | _ -> -1   // val it : int = -1
(|Int|_|)
the |_| marks a partial active pattern — it may not match.
option result
Some v matches and binds v; None falls through to the next arm.
fallback
because it can decline, a partial pattern needs a following arm (here | _) to stay exhaustive.

A complete active pattern names every case and stays exhaustive; a partial one matches when it can and steps aside when it cannot. Either way, the arm reads like a built-in pattern.

Classify an input

Pick a value below. An integer routes through (|Even|Odd|) to one of two cases; a string routes through (|Int|_|) and either binds a parsed int or declines to the fallback. The readout names the matching case and any bound value, and the static table holds all four answers.

the input routed to its matching active-pattern case

input 4 (|Even|Odd|) | Even complete · n % 2 = 0 | Odd complete · the other case | Int v partial · Some v, binds v | _ (no match) partial · None, falls through BINDS / RESULT "even"
4 · via (|Even|Odd|) → case | Even · match result = "even"
The four classifications, computed by hand (the no-JS fallback)
inputactive patterncase matchedbinds / result
4(|Even|Odd|)| Even"even"
7(|Even|Odd|)| Odd"odd"
"42"(|Int|_|)| Int vv = 42
"x"(|Int|_|)| _ (no match)-1 (fallback)

The same match syntax reads four ways. Even and Int are not built into F#; a definition adds each one — and once defined, they sit beside | 0 and | x :: rest as equals.

Active pattern

A function presented to the matcher as a named case — complete (a partition) or partial (an option).

F# today

| Even and | Int v read like built-in arms, computed by your code.

Where this leads

Pattern matching is now complete: fixed shapes, guards, and cases you name yourself. The next module shapes the data these patterns read — discriminated unions and records. A union declares its own cases, and a match over it is the natural reader; the partial pattern's option result here is itself a two-case union, which C1.08 takes up as Option and Result.

References

Sources

Related in this course