Skip to content

Patterns

Every pattern form Atoll accepts — wildcards, literals, tuples, structs, variants, slices, ranges, bindings, alternatives, type tests, and guards.

Updated View as Markdown

A pattern describes a value’s shape and names the parts to bind from it. One pattern language serves four constructs: destructuring bindings, if pattern := conditions, for pattern in / for pattern := loop heads, and match arms.

struct Point { x: int, y: int }

fn manhattan(p: Point): int {
    Point { x, y } := p
    return x.abs() + y.abs()
}

Bindings and wildcards

A fresh identifier matches anything and binds it. _ matches anything and binds nothing.

fn f(values: []int): int {
    mut count := 0
    for _ in values {
        count += 1
    }
    return count
}
fn classify(value: int): string {
    return match value {
        0 => "zero"
        other => "non-zero: ${other}"
    }
}

Whether a bare identifier is a new binding or a visible constant is resolved by the compiler: a name that resolves to a constant matches that constant’s folded value instead of binding.

const HTTP_OK = 200

fn label(status: int): string {
    return match status {
        HTTP_OK => "ok"
        other => "status ${other}"
    }
}

Literals

Boolean, integer, character, and string literals match by equality.

fn dispatch(command: string): int {
    return match command {
        "start" => 1
        "stop" => 2
        "" => 0
        _ => -1
    }
}
fn digit_class(c: char): string {
    return match c {
        '0' => "zero"
        'a' => "letter a"
        _ => "other"
    }
}
fn describe(flag: bool): string {
    return match flag {
        true => "on"
        false => "off"
    }
}

Avoid floating-point literal patterns where rounding makes exact equality an unstable rule; compare with a tolerance instead.

Ranges

Inclusive a..=b and exclusive a..b range patterns match membership.

fn status_class(status: int): string {
    return match status {
        100..=199 => "informational"
        200..=299 => "success"
        300..=399 => "redirect"
        400..=499 => "client error"
        _ => "server error"
    }
}

Ranges work over characters too:

fn kind(c: char): string {
    return match c {
        '0'..='9' => "digit"
        'a'..='z' => "lower"
        'A'..='Z' => "upper"
        _ => "other"
    }
}

Endpoints must be compile-time values of a compatible ordered type.

Tuples

A tuple pattern matches arity and destructures positions. The outer parentheses may be omitted where a comma is unambiguous — binding position and loop heads.

fn endpoint(): (string, int) {
    return ("localhost", 8080)
}

fn port(): int {
    (host, number) := endpoint()
    return number + host.len()
}
fn shipping(order: (int, bool)): string {
    return match order {
        (0, _) => "empty"
        (count, true) => "${count} items, express"
        (count, false) => "${count} items, standard"
    }
}

Structs and records

A struct pattern names the fields it inspects. Bare field binds a name equal to the field name; field: subpattern renames or destructures further.

struct User { id: int, name: string, active: bool }

fn summarize(user: User): string {
    User { id: user_id, name, active } := user
    return "${user_id}/${name}/${active}"
}

.. ignores the fields you did not list:

struct Request { method: string, path: string, body: string, token: string }

fn route(request: Request): string {
    Request { method, path, .. } := request
    return "${method} ${path}"
}

Field subpatterns can be literals or ranges, which makes the whole pattern refutable and so only usable where failure has a meaning:

struct Event { code: int, source: string }

fn triage(event: Event): string {
    return match event {
        Event { code: 0, source } => "heartbeat from ${source}"
        Event { code: 400..=499, .. } => "client fault"
        Event { code, .. } => "code ${code}"
    }
}

Anonymous records use the same shape without a type name, and match structurally:

fn total(): int {
    reading := { x: 3, y: 4 }
    return match reading {
        { x, y } => x + y
    }
}

Variants

Enum, Option, Result, and declared error variants use constructor-shaped patterns. Tuple-style variants bind positionally, struct-style variants bind by field name, and unit variants have no payload.

enum Shape {
    Circle(float)
    Rect { width: float, height: float }
    Empty
}

fn area(shape: Shape): float {
    return match shape {
        Circle(radius) => radius * radius * 3.14159
        Rect { width, height } => width * height
        Empty => 0.0
    }
}

Option and Result are ordinary variants:

error LoadError { NotFound }

fn load(id: int): int ! LoadError {
    if id < 0 { error NotFound }
    return id * 2
}

fn describe(id: int, cached: int?): string {
    hit := match cached {
        Some(value) => "cached ${value}"
        None => "cold"
    }
    outcome := match load(id) {
        Ok(value) => "loaded ${value}"
        Err(err) => "failed"
    }
    return "${hit} / ${outcome}"
}

Bind the error payload as err or e. error is a keyword and can never be an identifier, so Err(error) does not parse.

Qualify a variant with its type when a bare name would be ambiguous, or simply for clarity:

enum NetworkState { Ready, Down }

fn connect(state: NetworkState): int {
    return match state {
        NetworkState.Ready => 1
        NetworkState.Down => 0
    }
}

Nesting

Patterns compose to any depth, and the compiler checks coverage structurally.

enum Response {
    Loaded(int?)
    Failed(string)
}

fn describe(response: Response): string {
    return match response {
        Loaded(Some(value)) => "value ${value}"
        Loaded(None) => "empty"
        Failed(message) => "failed: ${message}"
    }
}
struct Address { city: string, zip: int }
struct Person { name: string, home: Address? }

fn city_of(person: Person): string {
    return match person {
        Person { home: Some(Address { city, .. }), .. } => city
        Person { name, home: None } => "${name} has no address"
    }
}

Slices

Slice patterns match a sequence by length and position. ..rest absorbs the remainder.

fn headline(values: []int): string {
    return match values {
        [] => "empty"
        [only] => "single ${only}"
        [first, second] => "pair ${first}/${second}"
        [head, ..tail] => "head ${head} plus more"
        _ => "unreachable"
    }
}

Slice patterns alone are not treated as covering a list, so keep a _ arm:

fn headline(values: []int): string {
    return match values {
        [] => "empty"
        [only] => "single ${only}"
        [head, ..tail] => "many"
    }
}

Bound patterns

name @ subpattern binds the whole matched value while still requiring the subpattern — no reconstructing a value from its parts just to keep the original.

fn checked(value: int): int {
    return match value {
        digit @ 0..=9 => digit
        big @ 1000..=9999 => big / 1000
        _ => -1
    }
}

Alternatives

left | right matches either side. Every alternative should bind the same names with compatible types, because the arm body sees one environment.

enum Event {
    Created(int)
    Updated(int)
    Deleted(int)
}

fn affected(event: Event): int {
    return match event {
        Created(id) | Updated(id) => id
        Deleted(id) => -id
    }
}

Alternatives combine with literals and ranges too:

fn weekend(day: string): bool {
    return match day {
        "sat" | "sun" => true
        _ => false
    }
}

Type patterns

name: Type tests an anonymous union’s constituent and binds the refined value.

fn render(value: int | string | bool): string {
    return match value {
        n: int => "int ${n}"
        s: string => "string ${s}"
        b: bool => "bool ${b}"
    }
}

The binding has the refined type inside the arm, so n is a plain int there.

Guards

if condition after a pattern adds a boolean requirement. The pattern binds first, then the guard runs; a false guard resumes matching at the next arm.

struct Order { total: int, expedited: bool }

fn fee(order: Order): int {
    return match order {
        Order { total, expedited: true } if total > 100 => 0
        Order { total: _, expedited: true } => 15
        Order { total, expedited: false } if total > 100 => 5
        _ => 10
    }
}

A guard never proves a variant exhausted, because arbitrary boolean logic cannot be shown to cover every value. Keep one unguarded arm for the remainder.

Refutability

An irrefutable pattern always matches its input type: a plain binding, _, a complete tuple destructure, a single-variant struct pattern. A refutable pattern can fail: variants, literals, ranges, slices with a fixed length.

Refutable patterns are only allowed where failure has defined control flow:

Context On match On failure
irrefutable binding initialize names not applicable
Pattern := value else { } names stay in scope after the statement run the diverging else
if Pattern := value run the then-branch with the names run the else-branch without them
for Pattern := value run one iteration with the names end the loop
for Pattern in iterable bind the yielded element element pattern must be irrefutable
match arm evaluate the guard, then the body try the next arm

The same Some(value) shape therefore behaves differently in each of these:

fn lookup(id: int): int? {
    if id < 0 { return None }
    return Some(id)
}

fn four_ways(id: int, ids: []int): int {
    mut total := 0

    // 1. condition — bindings live in the then-branch only
    if Some(value) := lookup(id) {
        total += value
    }

    // 2. refutable binding — bindings live for the rest of the block
    Some(base) := lookup(id) else {
        return -1
    }
    total += base

    // 3. loop head — failure ends the loop
    mut cursor := 0
    for Some(value) := lookup(cursor) {
        total += value
        cursor -= 1
    }

    // 4. match arm — failure tries the next arm
    for candidate in ids {
        total += match lookup(candidate) {
            Some(value) => value
            None => 0
        }
    }

    return total
}

Putting it together

enum Command {
    Get { key: string }
    Set { key: string, value: int }
    Delete(string)
    Batch([]string)
    Ping
}

struct Reply { code: int, text: string }

fn execute(command: Command, store: Map[string, int]): Reply {
    return match command {
        Ping => Reply { code: 200, text: "pong" }

        Get { key } => match store.get(key) {
            Some(value) => Reply { code: 200, text: "${value}" }
            None => Reply { code: 404, text: "missing ${key}" }
        }

        Set { key, value } if value < 0 =>
            Reply { code: 400, text: "negative value for ${key}" }

        Set { key, value } => Reply { code: 201, text: "${key}=${value}" }

        Delete(key) => Reply { code: 204, text: key }

        Batch([]) => Reply { code: 400, text: "empty batch" }

        Batch([only]) => Reply { code: 200, text: "batch of one: ${only}" }

        Batch(keys) => Reply { code: 200, text: "batch of ${keys.len()}" }
    }
}

Struct-payload variants, tuple payloads, guards, slice patterns, and a nested Option match all coexist in one decision. Ordering matters: the guarded Set arm has to precede the unguarded one, and the general Batch(keys) arm has to come last.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close