Pattern Matching
Match on values, destructure data, handle every case.
Basic Match
Pattern matching is one of Loon's most powerful features, and once you get used to it, you'll wonder how you ever lived without it. A match expression takes a value and compares it against a series of pattern-body pairs. Patterns and bodies alternate: odd positions are patterns, even positions are bodies.
[match status
"ok" [println "success"]
"error" [println "failed"]
_ [println "unknown"]]The patterns are tried in order, top to bottom. The first one that matches wins, and its right-hand side runs. Think of it as a more structured, more powerful alternative to long if/else chains.
Literal Patterns
The simplest patterns are literal values. Numbers, strings, booleans, keywords all work exactly as you'd expect.
[match n
0 "zero"
1 "one"
_ "many"]This is cleaner than a chain of if/else comparisons, and the compiler can check that your patterns are reasonable. You get clarity and safety at the same time.
Wildcard
The underscore _ matches anything. You'll use it in two situations: as a catch-all at the bottom of a match (like the default case in a switch statement), and inside structured patterns to ignore parts you don't 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 arm, it ignores the entire value. Same symbol, flexible meaning.
Variable Binding
When you put a name in a pattern instead of a literal or wildcard, Loon binds the matched value to that name. This lets you extract data and give it a meaningful label in one step.
[match [Some 42]
[Some x] [str "got " [str x]]
None "nothing"]Here x gets bound to 42 when the match succeeds. You can then use x in the arm's body like any other variable. This is pattern matching at its most useful: deciding what shape data has and pulling out the pieces you need, all in a single expression.
Constructor Patterns
Where pattern matching truly shines is with algebraic data types. When you define a type with multiple constructors, you can match on each constructor 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 has no data, so there's nothing to bind. The code reads almost like a specification of what each shape means.
Destructuring in Let
Patterns aren't limited to match expressions. You can use them on the left side of let bindings to pull apart vectors and maps 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 automatically. This keeps your code concise without sacrificing readability.
Destructuring in Function Params
Function parameters support the same destructuring patterns as let. This is especially nice when a function expects structured data, because the signature itself documents what shape the input should have.
[fn area [[Circle r]]
[* 3.14 r r]]
[fn full-name [{first last}]
[str first " " last]]The area function only accepts Circles. The full-name function expects a map with :first and :last keys. Both are self-documenting and concise.
Exhaustiveness
Here's where Loon's pattern matching really pays for itself. The compiler checks that every match expression covers all possible cases. If you forget a variant, you get a compile error, not a runtime crash six months later in production.
[type Color Red Green Blue]
; Compile error: missing Blue
[match c
Red "red"
Green "green"]This becomes incredibly valuable during refactoring. When you add a new variant to a type, the compiler immediately finds every match expression that needs updating. You don't have to grep through your codebase hoping you found them all.
Exhaustiveness checking makes refactoring safe. Add a variant to a type and the compiler finds every match that needs updating.