Pattern Matching

Match on values, destructure data, handle every case.

Basic Match

Pattern matching is among Loon's most powerful features, and few who learn it willingly go back. A match expression takes a value and compares it against a series of pattern-body pairs. The two alternate: odd positions are patterns, even positions are bodies.

[match status
  "ok"    [println "success"]
  "error" [println "failed"]
  _       [println "unknown"]]

Patterns are tried in order, top to bottom. The first one that matches wins, and its right-hand side runs. Read it as a structured, far more capable successor to the long if/else chain.

Literal Patterns

The simplest patterns are literal values. Numbers, strings, booleans, and keywords all match exactly as written.

[match n
  0 "zero"
  1 "one"
  _ "many"]

This reads more clearly than a chain of if/else comparisons, and the compiler can check that your patterns are reasonable. Clarity and safety arrive together.

Wildcard

The underscore _ matches anything. It earns its keep in two places: as a catch-all at the bottom of a match, the way a default case closes a switch, and inside structured patterns to ignore the parts you do not care about.

[match pair
  #[0 _] "starts with zero"
  #[_ 0] "ends with zero"
  _      "neither"]

In the first two arms, _ ignores one element of the vector. In the last, it ignores the entire value. One symbol, meaning shaped by where it sits.

Variable Binding

Put a name in a pattern, where a literal or wildcard might go, and Loon binds the matched value to it. You extract the data and give it a meaningful label in a single step.

[match [Some 42]
  [Some x] [str "got " [str x]]
  None     "nothing"]

Here x is bound to 42 when the match succeeds, and the arm's body uses x like any other variable. This is pattern matching at its most useful: asking what shape the data has and pulling out the pieces you need, in one expression.

Constructor Patterns

Pattern matching comes fully alive against algebraic data types. Define a type with several constructors, and you can match each one and extract the data it carries.

[type Shape
  [Circle Float]
  [Rect Float Float]
  Point]

[fn describe [s]
  [match s
    [Circle r]  [str "circle r=" [str r]]
    [Rect w h]  [str "rect " [str w] "x" [str h]]
    Point       "point"]]

Each pattern mirrors the constructor it matches. [Circle r] matches a Circle and binds its radius to r. [Rect w h] matches a Rect and binds both dimensions. Point carries no data, so there is nothing to bind. The code reads almost like a specification of what each shape means.

Destructuring in Let

Patterns are not confined to match. Place one on the left side of a let binding to pull vectors and maps apart directly.

[let #[a b c] #[1 2 3]]
[println b]  ; 2

[let {name age} {:name "Ada" :age 30}]
[println name]  ; "Ada"

Vector destructuring binds by position. Map destructuring binds by key name, so {name age} pulls out the :name and :age keys on its own. Concise, and no harder to read for it.

Destructuring in Function Params

Function parameters accept the same destructuring patterns as let. This pays off when a function expects structured data, since the signature itself spells out the shape of the input.

[fn area [[Circle r]]
  [* 3.14 r r]]

[fn full-name [{first last}]
  [str first " " last]]

The area function accepts only Circles. The full-name function expects a map with :first and :last keys. Each says what it needs in the act of asking for it.

Exhaustiveness

This is where pattern matching pays for itself. The compiler checks that every match expression covers every possible case. Forget a variant and you get a compile error now, not a runtime crash six months from now in production.

[type Color Red Green Blue]

; Compile error: missing Blue
[match c
  Red   "red"
  Green "green"]

The value compounds during refactoring. Add a new variant to a type, and the compiler points you to every match expression that needs updating. No grepping through the codebase, hoping you caught them all.

Tip

Exhaustiveness checking makes refactoring safe. Add a variant to a type and the compiler finds every match that needs updating.