jonnifyfsharp

Dive C1.05.2

Guards & destructuring

Add a test to an arm; pull a tuple apart on the way in.

A plain pattern recognises a shape, but sometimes the shape is not enough — two negative numbers and two positives share the structure of an int. A when guard attaches a boolean test to an arm: the arm fires only if the pattern matches and the guard is true. Or-patterns let several patterns share one result. And a pattern is not confined to match — it works in a let binding and in a function parameter, so let (x, y) = point binds both coordinates at once.

C1.05.2 · part 1

The when guard

The function describe sorts an int into three buckets. The first arm is a constant pattern for zero. The second is a variable pattern x with a guard when x < 0 — it matches any integer, but fires only when the guard holds. The last arm catches everything that remains. Because zero is handled first, the guarded arm and the wildcard partition the non-zero values into negative and positive.

let describe n =
    match n with
    | 0 -> "zero"
    | x when x < 0 -> "negative"   // guard: matches, then tests x < 0
    | _ -> "positive"

describe -3                    // val it : string = "negative"
describe 0                     // val it : string = "zero"
describe 5                     // val it : string = "positive"
when
a boolean test on an arm; the arm fires only when the pattern matches and the guard is true.
order
arms are tried top to bottom; the first whose pattern matches and whose guard holds wins.
fall-through
if a guard fails, matching continues at the next arm — the guarded arm does not consume the value.
A guard can run any boolean expression, but the compiler cannot see inside it. An arm guarded by when x < 0 does not count toward exhaustiveness, so a match built only from guarded arms may still warn FS0025 — a final unguarded arm (| _) closes the cover.
C1.05.2 · part 2

Or-patterns and destructuring

When several cases share a result, an or-pattern joins them with | inside a single arm. Each alternative must bind the same names (here none), so the arm's result can use them uniformly.

let isVowel c =
    match c with
    | 'a' | 'e' | 'i' | 'o' | 'u' -> true   // or-pattern: any of the five
    | _ -> false

isVowel 'e'                    // val it : bool = true
isVowel 'z'                    // val it : bool = false

A pattern also stands on the left of a let and in a function's parameter list. There it is irrefutable — it must always match — which suits a tuple or a record whose shape is fixed. The binding let (x, y) = point names both coordinates; dist destructures its argument directly in the parameter position.

let point = (3.0, 4.0)
let (x, y) = point            // x = 3.0, y = 4.0

let dist (x, y) = sqrt (x*x + y*y)   // destructure in the parameter
dist point                     // val it : float = 5.0

A guard refines a structural match with a test; an or-pattern merges cases; a destructuring let pulls a fixed shape apart. The pattern is the same tool in every position.

Evaluate describe n

Pick an n below. The evaluator walks the three arms in order — is it 0? then when x < 0? else the wildcard — and lights the first that takes the value. The static table holds the same three answers, so the result stands without the script.

describe n · three arms, tried in order

n = -3 | 0 constant pattern — is n exactly 0? · | x when x < 0 variable + guard — matches, then tests x < 0 · | _ wildcard — everything that remains ·
n = -3 · | 0 fails → | x when x < 0 matches and -3 < 0 holds · result = "negative"
The three evaluations, computed by hand (the no-JS fallback)
narm that fireswhyresult
-3| x when x < 0not 0; matches x, guard -3 < 0 holds"negative"
0| 0the constant pattern matches first"zero"
5| _not 0; guard 5 < 0 fails; wildcard takes it"positive"

Zero is caught by structure, the negatives by a guard, the rest by the wildcard. The order is the logic — move the wildcard up and the other arms never run.

Guard

A boolean test layered on a pattern, evaluated only after the pattern itself matches.

F# today

| x when x < 0 binds x, then fires only if the test on it holds.

Where this leads

Guards and or-patterns refine the cases F# supplies. The final dive goes further: an active pattern lets a programmer define a new case by name — a function that the matcher calls as if it were built in — so | Even and | Int v read like | 0 and | x :: rest do here.

References

Sources

Related in this course