SagaSaga
Guide

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 n

Any 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 _ = False

This 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         = items

This is convenient for functions that return tuples:

let (width, height) = measure img

If 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.