Skip to main content

Active Patterns

An active pattern is a user-defined match arm. It looks and feels like a constructor pattern, but the match logic is an ordinary function body — which means the matcher can compute, parse, or probe the input before deciding whether (and how) it matches.

This page documents:

  • How to declare an active pattern (pattern …).
  • How to use one in match arms.
  • The difference between total and partial patterns.
  • Pattern combinators (&, |).

A first example

The idiomatic F# "even numbers" pattern:

pattern Even(n: Int) -> Bool = n % 2 == 0;

fn describe(n: Int) -> Text {
match n {
0 => "zero",
Even() => f"{n} is even",
_ => f"{n} is odd",
}
}

The declaration pattern Even(n: Int) -> Bool = n % 2 == 0; reads: "Even is a pattern that takes an Int and matches when n % 2 == 0."

In the match arm, Even() carries no parentheses arguments because n is the match subject itself. The empty parens are the signal that this is a total pattern — it always reaches a yes/no decision and does not extract sub-values.

Declaration grammar

pattern_def = visibility , 'pattern' , identifier
, [ pattern_type_params ]
, '(' , pattern_params , ')'
, '->' , type_expr , '=' , expression , ';' ;

pattern_type_params = '(' , param_list , ')' ; (* parameters for the pattern *)
pattern_params = [ param , { ',' , param } ] ; (* the match subject(s) *)

The pattern's result type determines its category:

Result typeKindMeaning
BoolTotalDoes the subject match? No extraction.
Maybe<T>PartialIf the subject matches, extract a T.

Total patterns

Return Bool. Use them to test a condition as a pattern:

pattern Positive(n: Int) -> Bool = n > 0;
pattern Empty(t: &Text) -> Bool = t.is_empty();

match number {
Positive() => handle_positive(number),
_ => handle_nonpositive(number),
}

Combined with the & pattern-AND, they compose:

match number {
Positive() & Even() => "positive even",
Positive() & _ => "positive odd",
_ => "nonpositive",
}

& matches when both patterns match. Unlike | (or-pattern), & does not introduce any branch; it threads the same subject through two conditions.

Parameterised patterns

A pattern can take pattern parameters — values provided at the match site — in addition to the match subject. The grammar uses two parenthesised groups:

// pattern parameters match subject
// ↓ ↓
pattern InRange(lo: Int, hi: Int)(n: Int) -> Bool = lo <= n && n <= hi;

match temperature {
InRange(60, 72)() => "comfortable",
InRange(0, 32)() => "freezing",
InRange(90, 130)() => "hot",
_ => "extreme",
}

The first parenthesised group is the pattern parameters; the second is the match subject. Total parameterised patterns always use double parens in match arms: InRange(0, 100)().

Partial patterns

A pattern that returns Maybe<T> is partial: on match, it produces a T that the match arm can bind:

pattern ParseInt(s: &Text) -> Maybe<Int> = s.parse_int();

match input {
ParseInt()(n) => print(f"parsed {n}"),
_ => print("not a number"),
}

The second parens (n) contain a binding pattern — an ordinary pattern that binds whatever the partial pattern extracted.

Parameterised partial patterns work the same way:

pattern RegexMatch(re: Regex)(s: &Text) -> Maybe<List<Text>> =
re.captures(s).map(|caps| caps.groups());

match email {
RegexMatch(rx#"^([^@]+)@([^@]+)$")(groups) =>
print(f"user = {groups[0]}, domain = {groups[1]}"),
_ => print("invalid email"),
}

Or the lean form without parameters:

pattern HeadTail<T>(xs: &List<T>) -> Maybe<(T, List<T>)> =
if xs.is_empty() { Maybe.None }
else { Maybe.Some((xs[0], xs.slice_from(1))) };

match items {
HeadTail()((h, t)) => process(h, t),
_ => handle_empty(),
}

Using patterns in nested positions

Active patterns appear anywhere a pattern can appear — not just top level in match:

let [Positive() & Even() & first, ..] = numbers
else { fallback() };

for (InRange(0, 255)(byte), offset) in stream.enumerate() {
write_at(offset, byte);
}

if let ParseInt()(n) = text.trim() && n > 100 {
print(f"big number: {n}");
}

They compose with destructuring, let else, if-let chains, and stream patterns.

Interaction with guards

Active patterns replace most uses of if guards in match arms. The two forms are interchangeable but carry different intent:

// Guard form: condition is ad hoc, local to this arm.
match n {
x if x > 0 && x % 2 == 0 => "positive even",
_ => "other",
}

// Active pattern form: condition is named, reusable, compositional.
match n {
Positive() & Even() => "positive even",
_ => "other",
}

Prefer active patterns when the condition is reusable or appears in multiple sites; prefer guards for one-off checks.

Visibility

A pub pattern is exported like any other item. Private patterns are module-local. Patterns can be defined in protocol impls and inherit the implementer's generic parameters.

implement<T: Ord> List<T> {
pub pattern Sorted(xs: &Self) -> Bool =
forall i in 0..xs.len()-1. xs[i] <= xs[i+1];
}

match numbers {
List.Sorted() => "already sorted",
_ => "needs sort",
}

Generic patterns

Patterns can be generic over their subject's type:

pattern NonEmpty<T>(xs: &List<T>) -> Bool = !xs.is_empty();
pattern First<T>(xs: &List<T>) -> Maybe<T> = xs.first().copied();

match list {
First()(hd) => process(hd),
_ => handle_empty(),
}

Type parameters are declared with the ordinary generic syntax (angle brackets).

Exhaustiveness

Active patterns are opaque to the exhaustiveness checker — the compiler cannot, in general, prove that a set of active patterns covers all cases. You must include a catch-all _ => arm when active patterns are the only alternatives:

match n {
Even() => "even",
Odd() => "odd",
_ => unreachable(), // required even though Even & Odd logically cover Int
}

The unreachable() body is optimised away if the solver can prove the arms are complete under an @verify(formal) function — see Proof DSL.

Declaration cheatsheet

ShapeDeclarationMatch-site usage
Total, no paramspattern Even(n) -> Bool = n%2==0;Even()
Total, with paramspattern InRange(lo, hi)(n) -> Bool = lo<=n<=hi;InRange(0, 100)()
Partial, no paramspattern Parse(s) -> Maybe<Int> = s.parse_int();Parse()(n)
Partial, with paramspattern Match(re)(s) -> Maybe<...> = re.match(s);Match(rx#"...")(g)
Generic partialpattern First<T>(xs) -> Maybe<T> = xs.first().copied();First()(h)

Grammar

pattern_def = visibility , 'pattern' , identifier , [ pattern_type_params ]
, '(' , pattern_params , ')' , '->' , type_expr , '=' , expression , ';' ;

active_pattern = identifier , active_pattern_tail ;
active_pattern_tail
= '(' , ')' (* total, no params *)
| '(' , ')' , '(' , pattern_list_nonempty , ')' (* partial, no params *)
| '(' , expression_list , ')' , '(' , [ pattern_list ] , ')' ; (* with params *)

See also