Pattern Matching
Pattern matching is how you inspect and destructure values in Saga. It works in
case expressions, function equations, and let bindings. The compiler checks
every match for exhaustiveness, so you can't silently miss a case.
Case Expressions
A case expression matches a value against a list of patterns and evaluates the
first arm that matches:
type Shape =
| Circle Float
| Rect Float Float
| Point
area shape = case shape {
Circle r -> 3.14 * r * r
Rect w h -> w * h
Point -> 0.0
}Each arm is pattern -> expression. Patterns are tried top to bottom; the first
match wins.
Variables and Wildcards
A bare name matches any value and binds it for use in the branch. An underscore
_ also matches any value but discards it:
describe shape = case shape {
Circle r -> $"circle with radius {r}"
Rect _ _ -> "rectangle"
Point -> "point"
}A variable also works as a catch-all arm. Unlike _, it gives you the value to
use in the branch:
type Priority = Critical | High | Medium | Low
label priority = case priority {
Critical -> "page immediately"
High -> "fix today"
other -> $"queued ({other})"
}other matches Medium and Low and binds the actual value, so it can appear
in the expression. Use _ when you don't need it at all.
Guards
A when clause adds a condition to a pattern. The arm only fires if the pattern
matches and the guard expression is true:
classify n = case n {
n when n < 0 -> "negative"
n when n > 100 -> "large positive"
0 -> "zero"
n -> "positive"
}Guards work the same way on function equations:
fizzbuzz n when n % 15 == 0 = "FizzBuzz"
fizzbuzz n when n % 3 == 0 = "Fizz"
fizzbuzz n when n % 5 == 0 = "Buzz"
fizzbuzz n = show nAny pure expression works as a guard, including calls to other functions:
is_valid_length s = String.length s > 0 && String.length s < 100
describe s = case s {
s when is_valid_length s -> "valid: " <> s
_ -> "invalid"
}List Patterns
Lists match against their structure:
summarize items = case items {
[] -> "empty"
[_] -> "one element"
[_, _] -> "two elements"
_ :: _ -> "head and tail elements"
}The :: pattern separates the head from the tail. You can chain it to reach
deeper into the list:
first_two xs = case xs {
x :: y :: _ -> show x <> " and " <> show y
_ -> "fewer than two elements"
}Patterns compose, so you can nest them freely. A list of tuples, for example:
case pairs {
(a, b) :: rest -> $"first pair: {a}, {b}"
[] -> "empty"
}Tuple Patterns
Tuples match by position:
fun swap : (a, b) -> (b, a)
swap (x, y) = (y, x)label result = case result {
(True, value) -> "found: " <> show value
(False, _) -> "not found"
}Record Patterns
Records can be destructured in patterns and matched on individual fields. You only need to mention the fields you care about -- unmentioned fields are ignored:
record Point { x: Int, y: Int }
quadrant p = case p {
Point { x, y } when x > 0 && y > 0 -> "Q1"
Point { x, y } when x < 0 && y > 0 -> "Q2"
Point { x, y } when x < 0 && y < 0 -> "Q3"
Point { x, y } when x > 0 && y < 0 -> "Q4"
_ -> "origin or axis"
}To bind a field under a different name, use field: alias:
greet user = case user {
User { name: n, email: e } -> $"Hello {n}, your email is {e}"
}When you only care about which variant matched and not its fields, an empty pattern works:
is_authenticated session = case session {
LoggedIn {} -> True
Anonymous {} -> False
}String Patterns
Strings match exactly, or you can split off a prefix using <>:
classify_log msg = case msg {
"[ERROR]: " <> detail -> "Error: " <> detail
"[WARN]: " <> detail -> "Warning: " <> detail
"[INFO]: " <> detail -> "Info: " <> detail
_ -> "unknown: " <> msg
}The <> pattern always splits from the left -- you match a known prefix and
capture the rest. A wildcard arm is required because strings are infinite; unlike
ADTs, the compiler can't enumerate all possibilities.
Or-Patterns
Multiple patterns can share a single arm using |:
is_shaped shape = case shape {
Circle _ | Rect _ _ -> True
Point -> False
}Patterns in Function Parameters
Patterns can appear directly in function parameters, not just in case:
is_big (Circle r) when r > 10.0 = True
is_big (Rect w h) when w > 10.0 || h > 10.0 = True
is_big _ = FalseThis is equivalent to a case inside the function body but reads more naturally
when each variant has distinct behavior.
Let Destructuring
Patterns also work in let bindings:
let (x, y) = compute_origin ()
let Point { x, y } = get_anchor ()
let h :: t = itemsThis is convenient for functions that return tuples:
let (width, height) = measure imgIf the pattern might not match (like h :: t on a potentially empty list), use
case instead.
Exhaustiveness
The compiler verifies that every case expression covers all possible values. A
missing arm is a compile error:
# Error: missing Nothing case
case opt {
Just x -> x
}
# Fine
case opt {
Just x -> x
Nothing -> 0
}A wildcard or catch-all variable arm also satisfies the check:
case opt {
Just x -> x
_ -> 0 # covers Nothing (and any future variants)
}This applies to function equations too. Exhaustiveness checking means you can't have a runtime pattern match failure -- if the code compiles, every case is handled.