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

x

bind this position, or the whole subject, to x

literal

5, "s", foo:, 'sym

the position must equal the value

typed capture

x :: T

bind x; the value must satisfy the type or predicate T

type check

(:: T)

check only, no binding

sequence

#[a b c] or #(a b c)

destructure a sequence by position

rest

#[a *: rest]

bind the remaining elements to rest

mapping

#{KEY: PAT ...}

the value must be a mapping; see below

class

Point[x y]

an isinstance check plus field extraction

class, keyword fields

Point[x: 0 y]

attribute x must equal 0; bind attribute y

extraction

(app f PAT)

call f on the value and match PAT against the result

predicate

(? pred)

pred called on the value must be truthy

alternation

(or PAT PAT ...)

the first alternative that matches

negation

(not PAT)

the value must not match PAT

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 match form, like every other binding and like Python’s match. 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 OPTIONAL that did not fire, leaves its variable as it was, and a variable never bound raises NameError when 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 is None.

See also