jonnifyfsharp

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.

C3.03.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 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"
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. 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.
C3.03.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.

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 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.
C3.03.3 · part 3

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 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"
match 4 with via (|Even|Odd|) → | Even · returns "even"
Without JavaScript: the same four inputs, by hand
inputactive patternmatched caseresult
4(|Even|Odd|)| Even"even"
7(|Even|Odd|)| Odd"odd"
"42"(|Int|_|)| Int vv = 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

Related in this course