SCUA

Manual

Pattern matching

match looks at a value, finds the first pattern that fits, and runs that arm. It replaces long chains of if/else if and, for the closed types like enums, it checks that you've handled every case.

A match has a subject, one or more arms written pattern -> body, and ends with end:

fn describe(n)
  match n
    0            -> return "zero"
    x when x > 0 -> return "positive"
    _            -> return "negative"
  end
end
print(describe(0))
print(describe(7))
print(describe(-3))

Run it:

$ scua describe.scua
zero
positive
negative

Arms are tried top to bottom. The first one whose pattern matches wins, so order matters: put specific patterns before general ones.

#Arm bodies: one statement, or a do … end block

An arm body is a single statement — that's why every example here uses one return or print. When an arm needs to do several things, wrap them in a do … end block, which counts as one statement:

fn tick(state)
  let next = state
  match state
    "idle" -> do
      print("waking up")
      next = "running"
    end
    "running" -> do
      print("working")
      next = "done"
    end
    _ -> next = state          -- a single statement needs no block
  end
  return next
end
let s = "idle"
for _ in 0:3 do s = tick(s) end
print(`final: {s}`)
$ scua tick.scua
waking up
working
final: done

Without the block, a two-statement arm is a syntax error, because the second statement reads as the start of the next arm. State machines are the usual place you'll reach for do … end, since each arm tends to act and then compute the next state.

#Literals

A literal pattern matches a value equal to it. Numbers, strings, true, false, and nil all work:

match status
  200 -> print("ok")
  404 -> print("not found")
  _   -> print("other")
end

#Bindings and the wildcard

A bare name is a binding. It matches anything and names the value inside the arm. In the first example, x binds the subject so the guard can test it.

The wildcard _ also matches anything but binds nothing. Use it for the catch-all arm when you don't need the value.

#Tags: Ok and Error

Ok(x) and Error(e) are the two shapes of a Result. A tag pattern matches that tag and binds (or further matches) its payload. The payload can itself be a pattern, including a literal:

fn parse(r)
  match r
    Ok(0)    -> return "ok, zero"
    Ok(n)    -> return `ok {n}`
    Error(e) -> return `error: {e}`
  end
end
print(parse(Ok(0)))
print(parse(Ok(42)))
print(parse(Error("bad input")))
$ scua parse.scua
ok, zero
ok 42
error: bad input

Ok(0) only matches when the payload is exactly 0; Ok(n) catches every other Ok and binds the payload to n.

#Enum variants

A variant is matched by its qualified name. If the variant carries a payload, destructure it positionally:

enum AIState {
  Idle,
  Patrol(int),
  Chase(target: int, dist: int),
  Dead,
}
fn behave(s)
  match s
    AIState.Idle        -> return "wander"
    AIState.Patrol(wp)  -> return `patrol to {wp}`
    AIState.Chase(t, d) -> return `chase #{t} at range {d}`
    AIState.Dead        -> return "respawn"
  end
end
print(behave(AIState.Idle))
print(behave(AIState.Patrol(7)))
print(behave(AIState.Chase(42, 3)))
print(behave(AIState.Dead))
$ scua behave.scua
wander
patrol to 7
chase #42 at range 3
respawn

The field names on Chase are documentation. You match the payload by position, not by name. See enums for more.

#Array patterns

An array pattern matches by length and binds the elements. [a, b] matches an array of exactly two. [head, ..tail] matches one-or-more, binding the first element and a fresh slice of the rest. A bare .. matches the rest without binding anything:

fn summarize(xs)
  match xs
    []       -> return "nothing"
    [only]   -> return `just {only}`
    [a, b]   -> return `a pair: {a} and {b}`
    [h, ..t] -> return `{h}, then {t.len()} more`
    _        -> return "not a list"
  end
end
print(summarize([]))
print(summarize([99]))
print(summarize([1, 2]))
print(summarize([10, 20, 30, 40]))
print(summarize("hello"))
$ scua summarize.scua
nothing
just 99
a pair: 1 and 2
10, then 3 more
not a list

The patterns are 0-based slices. An array pattern also tests that the subject is an array, so passing the string "hello" falls through to the wildcard.

#Table and record patterns

A table pattern matches fields by name. {x} is shorthand for "has a field x, bind it to x." {key = pattern} matches the field against a nested pattern, and a literal there refutes the arm if it doesn't match:

fn render(shape)
  match shape
    { kind = "circle", r = r }      -> return `circle r={r}`
    { kind = "rect", w = w, h = h } -> return `rect {w}x{h}`
    { x, y }                        -> return `point ({x}, {y})`
    _                               -> return "unknown shape"
  end
end
print(render({ kind = "circle", r = 5 }))
print(render({ kind = "rect", w = 3, h = 4 }))
print(render({ x = 1, y = 2 }))
$ scua render.scua
circle r=5
rect 3x4
point (1, 2)

A table pattern only names the fields it cares about; extra fields on the subject are fine. The same syntax works on records.

Patterns nest freely. A tag whose payload is a table, or an array of tagged values, is just patterns inside patterns:

match ev
  Ok({ url = u, code = 200 }) -> return `200 {u}`
  Ok({ code = c })            -> return `ok ({c})`
  Error([first, ..])          -> return `first error: {first}`
  _                           -> return "?"
end

#Guards with when

A when clause adds a boolean test to an arm. The arm matches only if the pattern fits and the guard is true; otherwise matching falls through to the next arm:

match n
  x when x > 0 -> return "positive"
  _            -> return "non-positive"
end

#Alternation with |

| lets one arm cover several patterns:

fn category(n)
  match n
    0 | 1 | 2 -> return "small"
    3 | 4 | 5 -> return "medium"
    _         -> return "large"
  end
end
print(category(1))
print(category(4))
print(category(9))
$ scua category.scua
small
medium
large

The branches of a | can't bind a name, because the different branches would bind different things. The compiler stops you:

$ scua altbind.scua
scua: altbind.scua:2: a `|` pattern cannot bind a name — split it into separate arms

#Exhaustiveness

When the subject is a closed enum, the compiler checks that your arms cover every variant. Leave one out and it tells you which:

enum Dir { North, East, South, West }
fn label(d)
  match d
    Dir.North -> return "up"
    Dir.East  -> return "right"
    Dir.South -> return "down"
  end
end
print(label(Dir.North))
$ scua label.scua
scua: label.scua:3: type error: match on Dir is not exhaustive — missing: West

Add the missing arm, or a _ catch-all, and it compiles. This check is what makes match over an enum safe to refactor: add a variant and every match that needs updating becomes a compile error instead of a surprise at runtime.

#When no arm matches at runtime

If the subject isn't a checked enum (a number, a table, an untyped value), the compiler can't prove the match is complete. If none of the arms fit at runtime, the match raises a fault:

fn classify(n)
  match n
    1 -> return "one"
    2 -> return "two"
  end
end
print(classify(1))
print(classify(99))
$ scua classify.scua
one
scua: classify.scua:4: match: no arm matched (got 99)
  at classify (line 4)
  at main (line 8)

For matches on dynamic data, add a _ arm so there's always a fallback.

#See also