Pattern Matching

<!-- audited: 2026-09-23 -->

match dispatches on the structure and value of data. Arms are tried top to bottom; the first pattern that matches (and whose guard, if any, passes) selects the body. If no arm matches, a runtime :match-error is raised carrying the unmatched value. A catch-all final arm — a wildcard (_) or a variable pattern — is idiomatic when a fallback makes sense, but it is not required: omitting it means "no other value can reach this match", and the runtime error enforces that claim.

The compiler errors on unreachable arms — an arm that earlier arms already cover (a duplicated literal, or anything after a guardless catch-all) is rejected at compile time.

Basic patterns

Literal values (numbers, keywords, strings, booleans) match by equality:

(defn describe [val]
  (match val
    0      "zero"
    1      "one"
    _      "other"))

(assert (= (describe 0) "zero"))
(assert (= (describe 1) "one"))
(assert (= (describe 42) "other"))

Binding patterns

Unbound symbols in patterns bind the matched value — they do not compare against variables in scope. Use case (see control.md) for equality dispatch against evaluated expressions.

(defn first-or-default [lst fallback]
  (match lst
    (x & _) x
    _       fallback))

(assert (= (first-or-default (list 10 20) :none) 10))
(assert (= (first-or-default (list) :none) :none))

Important: a bare symbol always binds, never compares:

(def x 42)
(assert (= (match 99
             x x)       # x binds to 99 — NOT a comparison with 42
           99))

A bare symbol is a catch-all, so no arm may follow it — the compiler rejects unreachable arms.

To dispatch against a variable's value, use case or a guard:

(def quit-code 0x100)

(defn by-case [etype]
  (case etype               # case evaluates its keys and compares with =
    quit-code :quit
    :other))

(defn by-guard [etype]
  (match etype
    t when (= t quit-code) :quit   # a guard compares explicitly
    _ :other))

(assert (= (by-case 256) :quit))
(assert (= (by-guard 256) :quit))
(assert (= (by-guard 1) :other))

Array and struct patterns

(defn point-type [p]
  (match p
    [0 0]    :origin
    [x 0]    :x-axis
    [0 y]    :y-axis
    [x y]    :general
    _        :unknown))

(assert (= (point-type [0 0]) :origin))
(assert (= (point-type [5 0]) :x-axis))
(assert (= (point-type [3 4]) :general))

Struct patterns match by key, with literal values for dispatch:

(defn area [shape]
  (match shape
    {:type :circle :radius r}  (* 3.14159 r r)
    {:type :square :side s}    (* s s)
    _                          0))

(assert (= (area {:type :circle :radius 5}) 78.53975))
(assert (= (area {:type :square :side 7}) 49))

Nested patterns

Patterns compose to any depth:

(defn db-host [config]
  (match config
    {:db {:host h}} h
    _               "unknown"))

(assert (= (db-host {:db {:host "pg.local"}}) "pg.local"))
(assert (= (db-host {:nodb true}) "unknown"))

Or-patterns

(or p1 p2 ...) matches if any alternative matches. All alternatives must bind the same set of variables (or none at all).

(defn parity [n]
  (match n
    (or 1 3 5 7 9) :odd
    (or 0 2 4 6 8) :even
    _              :out-of-range))

(assert (= (parity 3) :odd))
(assert (= (parity 4) :even))
(assert (= (parity 42) :out-of-range))

(defn classify-suit [suit]
  (match suit
    (or :hearts :diamonds) :red
    (or :clubs :spades)    :black
    _                      :unknown))

(assert (= (classify-suit :hearts) :red))
(assert (= (classify-suit :spades) :black))

Or-patterns work with binding patterns — each alternative must bind the same names:

(defn first-element [coll]
  (match coll
    (or [x & _] (x & _))  x
    _                      nil))

(assert (= (first-element [10 20]) 10))
(assert (= (first-element (list 30 40)) 30))

Guards

when inside an arm adds a condition. The syntax is (pattern when condition body) — when is a bare keyword between the pattern and the body, not wrapped in parentheses:

(defn classify [n]
  (match n
    x when (> x 0) :positive
    0               :zero
    x               :negative))

(assert (= (classify 5) :positive))
(assert (= (classify 0) :zero))
(assert (= (classify -3) :negative))

Guards can reference bindings from the pattern:

(defn describe-pair [p]
  (match p
    [a b] when (> a b) "descending"
    [a b] when (= a b) "equal"
    [a b]              "ascending"
    _                  "not a pair"))

(assert (= (describe-pair [2 1]) "descending"))
(assert (= (describe-pair [1 1]) "equal"))
(assert (= (describe-pair :x) "not a pair"))

When a guarded arm's pattern is an or-pattern, a failed guard retries the remaining alternatives — each alternative re-binds and re-tests the guard before the match moves on to the next arm:

# alternative 1 binds x to the head (:a), the guard fails;
# alternative 2 retries with x bound to the tail (5) and passes
(assert (= (match (pair :a 5)
             (or (x . _) (_ . x)) when (= x 5) x
             _ :none) 5))

No matching arm

When no arm matches, match raises a :match-error carrying the unmatched value. Catch it with protect (or a try handler) like any other error:

(def [ok? err] (protect (match 5
                          1 :one
                          2 :two)))

(assert (not ok?))
(assert (= (get err :error) :match-error))
(assert (= (get err :value) 5))

A guard that fails on the final arm falls through the same way:

(def [positive? rejected] (protect (match -1
                                     x when (> x 0) :positive)))
(assert (not positive?))
(assert (= (get rejected :error) :match-error))

A match that can fail this way is typed as possibly erroring: signal inference marks it with :error unless some guardless arm is irrefutable (a wildcard or variable). Inside a (silent!) function, use a catch-all arm — a match without one is a compile-time signal violation.

(defn compiles? [src]
  (first (protect (compile/whole-module src "<doc>"))))

(assert (not (compiles? "(defn f [n] (silent!) (match n 1 :one 2 :two))")))
(assert (compiles? "(defn f [n] (silent!) (match n 1 :one _ :two))"))

Unreachable arms

An arm that earlier arms already cover can never match — the compiler rejects it with "unreachable match arm 2":

(assert (not (compiles? "(defn f [n] (match n _ :anything 1 :one))")))
(assert (not (compiles? "(defn f [n] (match n 1 :one 1 :uno _ :other))")))

Guarded arms never make later arms unreachable — the guard may fail at runtime, so the compiler assumes both outcomes are possible:

(assert (= (match 1
             x when false :never
             1            :one) :one))

The same analysis applies inside or-patterns, at any nesting depth: each alternative must match something that earlier arms and earlier alternatives do not. A dead alternative is a compile error. Below, arm 1 already matches 1, and in the second source every pair matches the first alternative, so the second is dead:

(assert (not (compiles? "(defn f [n] (match n 1 :one (or 1 2) :other))")))
(assert (not (compiles? "(defn f [p] (match p (or (x . _) (_ . x)) x))")))

On a guarded arm, earlier alternatives of the same or-pattern never make later ones dead: a failed guard retries the remaining alternatives (see Guards above), so (or (x . _) (_ . x)) when (= x 5) is legal — the second alternative is reachable through guard fallthrough. Coverage by earlier arms still applies to guarded arms as usual.

match vs case vs cond

matchcasecond
Dispatchstructural patternsequality (=) against evaluated expressionsarbitrary test expressions
Variablesbare symbols bindkeys are evaluated and comparedfull expressions
No matchruntime :match-error; unreachable arms are compile errorsthe default, else nilthe default, else nil
Use whendispatching on shape, type, or literal valuesdispatching against runtime valuesmulti-branch boolean logic
(def event-quit 12)
(def event-key-down 768)

(defn by-type [event-type]
  (match event-type          # match: literal keyword patterns
    :quit      :bye
    :key-down  :typed
    _          nil))

(defn by-code [raw-event-code]
  (case raw-event-code       # case: keys are variables holding event codes
    event-quit      :bye
    event-key-down  :typed
    :unknown))

(defn size [x]
  (cond                      # cond: arbitrary boolean conditions
    (> x 10) :large
    (> x 0)  :small
    (= x 0)  :zero
    :negative))

(assert (= (by-type :quit) :bye))
(assert (= (by-code 768) :typed))
(assert (= (size -4) :negative))
(assert (nil? (case 5 1 :a 2 :b)))

When cond branches are all testing the same expression against literal values, match is more concise: it rejects unreachable arms at compile time and raises a :match-error at runtime when no arm covers the value, instead of silently falling through. See control.md for cond and case.


See also