Match patterns
match destructures a value against a series of case patterns: the
first arm whose pattern matches binds its captured names and runs its
body. It is the sibling of
switch, which dispatches on
equality. match is for the shape of a value; switch compares it
against candidates.
match msg:
case #[(tag :: keyword) (id :: int) *: args]:
(handle tag id args)
case #{"kind": "job" "url": u}:
(fetch u)
case Point[x y]:
(plot x y)
else:
(ignore msg)
The form itself is compile/match
and layout/match.
Patterns are classified by head, not by bracket
() and [] are value-equal, so a pattern’s kind is never decided by
its bracket. Each kind announces itself with a head symbol or marker,
and the reader sugars #[...], #{...}, and Name[...] produce those
heads. A bare symbol is a capture; a bare atom is a literal; everything
structured says what it is.
Element vocabulary
Pattern |
Written |
Meaning |
|---|---|---|
capture |
|
bind this position, or the whole subject, to |
literal |
|
the position must equal the value |
typed capture |
|
bind |
type check |
|
check only, no binding |
sequence |
|
destructure a sequence by position |
rest |
|
bind the remaining elements to |
mapping |
|
the value must be a mapping; see below |
class |
|
an isinstance check plus field extraction |
class, keyword fields |
|
attribute |
extraction |
|
call |
predicate |
|
|
alternation |
|
the first alternative that matches |
negation |
|
the value must not match |
Patterns nest: a sequence element may be a class pattern, a mapping
value may be a sequence, an extraction result may be destructured, as
in (app splitext #[base "." "ki"]).
The matcher heads from koskari.matcher,
FORM, ONEOF, REST, CLASS, and the rest, are also accepted in
pattern position; the sugared forms above lower to them. (? pred) is
also a matcher head, the alias of PREDICATE, so it keeps its meaning
inside a head such as REPEAT. A name inside
a REPEAT that is not itself captured binds the list of its values,
one per pass: #[(REPEAT (FORM (MATCH k v)))] against
#(#(1 2) #(3 4)) binds k to [1, 3] and v to [2, 4]. In a
pattern built with the koskari.matcher API, a SAME of that name
after the REPEAT compares against the list.
Sequences
A sequence pattern is the explicit #[...] or #(...) form; a bare
list is never a sequence. The length must agree unless a *: rest
marker captures the remaining elements. The length is checked before
any element is tested: when the subject’s length rules a sequence
pattern out, no predicate or extractor in it is called, so
#[(? p) b] against #(1 2 3) misses without calling p. Inside a
sequence a typed element is parenthesized: #[(x :: int) y].
At the end of a sequence there is no element left to test. (not PAT)
is one element that does not match PAT, so it misses there:
#[a (not 2)] does not match #(1). The NOT head is zero width: it
consumes nothing and succeeds where PAT does not match, so
#[a (NOT 2)] matches #(1). (OPTIONAL PAT) at the end matches zero
elements, so (NOT (OPTIONAL PAT)) misses there.
Only sequences destructure. A sequence pattern matches lists, tuples,
and other collections.abc.Sequence instances such as range, the
same rule as Python’s PEP 634. str, bytes, and bytearray never
destructure into characters; mappings never destructure into their keys,
which belong to #{...} arms; sets are not sequences; and an iterator
that reaches a sequence arm is not consumed. Each of these is a
no-match, so a string still matches as a whole value, as a literal, a
capture, or s :: str, and a dict still matches mapping arms.
Classes
Point[x y] checks isinstance(value, Point) and extracts fields.
Positional fields resolve through the class’s __match_args__, which
dataclasses and named tuples provide on Python 3.10 and later; on 3.9,
which predates __match_args__, Koskari synthesizes the equivalent for
dataclasses and namedtuples, and any other class needs an explicit
__match_args__. Keyword fields, Point[x: 0 y], read attributes and
work on any class. Field patterns nest: Point[(x :: int) y]. For an
isinstance check with no field extraction, write the type-check pattern
(:: Point); an empty bracket group is an index suffix to the reader,
not a pattern.
Mappings
#{KEY: PAT ...} matches a dict or any collections.abc.Mapping.
Every listed key must be present and its value must match the paired
pattern; extra keys are ignored. A literal key is written as a literal,
#{"kind": "job"}. A symbol key is resolved from the enclosing scope at
match time, so keys can be computed:
var field "url"
match msg:
case #{field: u}: ; the key is whatever field holds, "url"
(fetch u)
Names resolve from the enclosing scope
A name a pattern references, the T in x :: T, the class in
Point[...], the pred in (? pred), the f in (app f ...), a
symbol mapping key, is an ordinary binding resolved from the enclosing
scope at match time. Closure-captured and local names work as they
would in the body.
Seven type names are the exception: int, str, list, tuple,
pair, symbol and keyword in a pattern always mean the builtin
types. The pattern tests them directly, so a match arm reads nothing
from the namespace for them, and rebinding one of those names in the
enclosing scope does not change what the pattern tests:
var int float ; does not reach the pattern below
match 2.5:
case x :: int:
"unreached: int in a pattern is the builtin"
case x:
"taken"
To test against your own type, bind it under another name.
Callables are called as functions
A predicate or extractor is an ordinary callable, and a plain Koskari
def works without wrapping:
def splitext [name]:
(return (name.rpartition "."))
match path:
case (app splitext #[base "." "ki"]):
(compile-source base)
The call is a call from the function the match is in, so a
predicate or extractor that awaits does so in place, as a call in the
arm body would. A predicate or extractor that raises propagates the
error rather than counting as a no-match.
Guards
A when or unless guard runs after the pattern has matched and bound,
with the captured names in scope. A failed guard moves to the next arm:
match v:
case #[a b] when (> a b):
"descending"
case #[a b]:
"ascending-or-equal"
Captured names stay bound when a guard fails, as after Python’s
case ... if ....
Binding
Captured names are bound in the enclosing scope and stay visible after the
matchform, like every other binding and like Python’smatch. Arms are not scopes.A capture is stored into its variable as the pattern walks the value, so an arm that misses after binding some captures leaves them bound, as does an
(or ...)alternative that misses after binding.Captures are sparse: a capture that did not bind, under an
OPTIONALthat did not fire, leaves its variable as it was, and a variable never bound raisesNameErrorwhen read. Bind a default before the match, or guard on the shape, when a slot may be absent:define unit "px" match #(12): case #[n (OPTIONAL unit)]: (#str n unit) ; "12px"; on #(12 "em"), "12em"
Arms are tried in order and the first match wins. With no matching arm and no
else:, the value isNone.
See also
compile/match: the special formlayout/match: thematch:blockcompile/switch: equality dispatchkoskari.matcher: the matcher these patterns compile toMacros and hygiene: the same matcher matching source forms at compile time