Dive C3.03.3
Active patterns
In Elixir you write the guard inline every time. In F# you can name it once and match on it.
This is the second power, and the one with no direct Elixir equivalent. An
active pattern is a small function in banana brackets (| … |) that the
matcher calls and treats as a pattern. Where an Elixir developer reaches for a cond or a
chain of guard clauses, F# lets you name the test once and match on it like a built-in case. A
complete active pattern such as (|Even|Odd|) sorts every input into one
of its named cases and stays exhaustive; a partial one such as (|Int|_|)
returns an option and may decline. Either way, the match arm reads like any other —
| Even, | Int v — and that is the point.
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 read | 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" match 4 with | Even -> "even" | Odd -> "odd" // val it : string = "even"
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. Because every input lands in exactly one case, the match over the
pattern is exhaustive: no FS0025.- (| … |)
- 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.
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.
Elixir — the guard, inline at the call site
# Elixir — written at each call site case Integer.parse(s) do {v, ""} -> v _ -> -1 end
F# — name the test once with a partial active pattern (verified)
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 vmatches and bindsv;Nonefalls through to the next arm.- fallback
- because it can decline, a partial pattern needs a following arm (here
| _) to stay exhaustive.
Parameterized — a guard library, named once
An active pattern can take arguments before the value, so one definition serves many cases. This is the F#
answer to a chain of Elixir cond clauses or repeated rem(n, k) ==
0 guards: write the test once, then match on it with different arguments. FizzBuzz is the small,
complete example — verified with dotnet fsi.
Elixir — a cond, each guard repeated
# Elixir def fizzbuzz(n) do cond do rem(n, 15) == 0 -> "FizzBuzz" rem(n, 3) == 0 -> "Fizz" rem(n, 5) == 0 -> "Buzz" true -> Integer.to_string(n) end end
F# — one parameterized pattern, matched with arguments (verified)
let (|DivBy|_|) k n = if n % k = 0 then Some () else None // val (|DivBy|_|) : k:int -> n:int -> unit option let fizzbuzz n = match n with | DivBy 15 -> "FizzBuzz" | DivBy 3 -> "Fizz" | DivBy 5 -> "Buzz" | _ -> string n // fizzbuzz 15, 9, 10, 7 -> FizzBuzz Fizz Buzz 7
DivBy 15 reads as a pattern but runs a function. Order matters exactly as
in an Elixir cond: the first matching arm wins, so DivBy 15 must
precede DivBy 3 and DivBy 5, or 15 would match
Fizz first.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 — the guard you would repeat in Elixir, named once.
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, computed by a pure
function over a fixed input set; the static table holds all four answers.
the input routed to its matching active-pattern case
| input | active pattern | matched case | result |
|---|---|---|---|
| 4 | (|Even|Odd|) | | Even | "even" |
| 7 | (|Even|Odd|) | | Odd | "odd" |
| "42" | (|Int|_|) | | Int v | v = 42 |
| "x" | (|Int|_|) | | _ | -1 (fallback) |
The case name reads like a built-in, but it runs your logic. That is the whole device: the test you would inline in an Elixir guard, lifted into a name you can match on.
Where this leads
That closes the module. Matching crossed unchanged from Elixir; exhaustiveness moved the missing-case check to
compile time; active patterns extended the pattern language with cases of your own. The next module returns to
the operator an Elixir developer knows best — the same |> pipe — and adds forward
composition >> on top of it.
References
Sources
- Microsoft — F# documentation — active patterns: complete, partial, and parameterized forms.
- FSharp.Core API reference — the
Choice<_,_>andOptiontypes behind active patterns. - Microsoft — .NET documentation —
Int32.TryParseand the BCL methods wrapped here.
Related in this course
- C1.05 — Pattern matching & active patterns — active patterns in full language depth.
- C3.03.2 — Exhaustiveness — the compile-time check this builds on.
- Elixir — The match operator —
caseand guards on the BEAM, the habit this extends.