Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pattern Matching

AetherShell provides match expressions for destructuring values and branching on their structure. Pattern matching works with all value types including arrays, records, and constructor-style tagged values.

Basic Match

let x = 42

match x {
    0 => "zero",
    1 => "one",
    _ => "something else"
}
# => "something else"

The _ wildcard matches anything without binding a name.

Binding Patterns

Identifier patterns match any value and bind it to a name:

let result = match get_status() {
    "ok" => "all good",
    "error" => "something broke",
    other => "unexpected: ${other}"
}

Literal Patterns

Match against exact values:

match value {
    42 => "the answer",
    "hello" => "greeting",
    true => "affirmative",
    null => "nothing",
    _ => "default"
}

Supported literal patterns:

PatternMatches
42Exact integer
"hello"Exact string
true / falseExact boolean
nullNull value

Array Patterns

Destructure arrays by matching their elements:

match [1, 2, 3] {
    [] => "empty",
    [x] => "one element: ${x}",
    [x, y] => "two: ${x}, ${y}",
    [x, y, z] => "three: ${x}, ${y}, ${z}",
    _ => "more than three"
}
# => "three: 1, 2, 3"

Array patterns require an exact length match. [x, y] won’t match a 3-element array.

Patterns can be nested:

match [[1, 2], [3, 4]] {
    [[a, b], [c, d]] => a + b + c + d,
    _ => 0
}
# => 10

Record Patterns

Match records by their fields:

let person = {name: "Ada", age: 36, role: "engineer"}

match person {
    {name: "Ada", role} => "Found Ada, role: ${role}",
    {name, age} => "${name} is ${age} years old",
    _ => "unknown"
}
# => "Found Ada, role: engineer"

Shorthand: {name} is equivalent to {name: name} — it matches the field name and binds its value to variable name.

Record patterns match if all specified fields exist. Extra fields are ignored:

# This matches even though the record has 'role' too
match {name: "Ada", age: 36, role: "engineer"} {
    {name, age} => "${name}, ${age}",
    _ => "no match"
}
# => "Ada, 36"

Constructor Patterns

AetherShell uses tagged records to represent algebraic data types. The Some and None constructors create tagged records:

let result = Some(42)    # => {_tag: "Some", _value: 42}
let empty = None         # => {_tag: "None"}

Match on constructors:

match Some(42) {
    Some(x) => "got value: ${x}",
    None => "nothing",
    _ => "unexpected"
}
# => "got value: 42"

Zero-argument constructors just check the _tag:

match None {
    Some(x) => "got ${x}",
    None => "nothing here",
}
# => "nothing here"

Guards

Add conditions to match arms with if:

match value {
    x if x > 100 => "large",
    x if x > 10 => "medium",
    x if x > 0 => "small",
    0 => "zero",
    x => "negative: ${x}"
}

Guards are evaluated with the pattern’s bindings in scope. If the guard is falsy, the next arm is tried:

match {name: "Ada", age: 36} {
    {name, age} if age >= 21 => "${name} is an adult",
    {name, age} => "${name} is ${age} years old",
    _ => "unknown"
}
# => "Ada is an adult"

Exhaustiveness

If no arm matches the value, a runtime error is produced:

Error: match: no arm matched the value

Always include a _ wildcard as the last arm to handle unexpected cases:

match status {
    "active" => handle_active(),
    "paused" => handle_paused(),
    _ => throw "Unknown status: ${status}"
}

Match as Expression

match is an expression — it returns a value:

let label = match count {
    0 => "none",
    1 => "one",
    _ => "many"
}

print label

Practical Examples

Implicit scrutinee in lambdas

When match is used inside a lambda body, the scrutinee can be omitted — it defaults to the lambda’s first parameter:

# Explicit scrutinee (always works)
let grade = fn(score) => match score {
    90..100 => "A",
    80..89  => "B",
    _       => "C"
}

# Implicit scrutinee (same result, cleaner)
let grade = fn(score) => match {
    90..100 => "A",
    80..89  => "B",
    _       => "C"
}

grade(85)  # "B"

This works with any single-parameter lambda, including in pipelines:

[1, 2, 3, 100] | map fn(x) => match {
    _ if x > 50 => "big",
    _            => "small"
}
# => ["small", "small", "small", "big"]

Note: match expr { ... } with an explicit scrutinee is always supported. The implicit form only applies inside lambdas and uses the first parameter.

Processing command output

let files = ls "."

files | map fn(f) => match {
    {is_dir: true, name} => "📁 ${name}/",
    {ext: "rs", name} => "🦀 ${name}",
    {ext: "md", name} => "📝 ${name}",
    {name} => "   ${name}"
}

Option handling

let find_user = fn(id) =>
    if id == 1 { Some({name: "Ada", role: "admin"}) }
    else { None }

match find_user(1) {
    Some({name, role}) => "Found ${name} (${role})",
    None => "User not found"
}
# => "Found Ada (admin)"

HTTP response handling

let response = http_get "https://api.example.com/status"

match response {
    {status: 200, body} => from_json(body),
    {status: 404} => throw "Not found",
    {status} => throw "HTTP error: ${status}"
}