Albite

Control flow and pattern matching

if

if is an expression, so it produces a value and both branches must have the same type:

let label = if count == 1 { "item" } else { "items" };

else if chains as you would expect:

fn describe(n: Nat) -> String {
    if n == 0 { "empty" } else if n < 10 { "small" } else { "large" }
}

An if used as a statement, with no else, must have type Unit — see Functions for how that interacts with return.

match

match takes a value apart by constructor:

fn describe(opt: Option<Nat>) -> String {
    match opt {
        None() => "nothing",
        Some(n) => "got a number",
    }
}

Arms are separated by commas. Bind a constructor’s payload by naming it, ignore it with _, or catch everything remaining with a bare _:

match value {
    SpecificCase => 1,
    _ => 0,
}

Exhaustiveness is checked

Every constructor must be covered, or a wildcard must be present. A missing case is a compile error rather than a runtime failure — which is the main reason to prefer a sum type over a sentinel value.

Nullary constructors

A constructor with no arguments can be written bare in a pattern — Nil => — as long as some other constructor of the same type carries a payload. None() with empty parentheses is always accepted and is the form you will see most in existing code. When no constructor of the type has a payload — type Color = Red | Green | Blue — the parentheses are required: a bare Red => is refused, with the misleading message match on non-sum types` is not supported.

Matching on numbers

Since 2.0-alpha

A match on a Nat or an Int takes literal patterns, | between literals, and if guards, and needs one catch-all arm without a guard. See Signed integers.

Nested patterns

Since 1.5 — v1.0 allowed only one level of destructuring, so this had to be written as nested match expressions.

Patterns nest to arbitrary depth, including tuples inside constructors:

fn unwrap_twice(o: Option<Option<Nat>>) -> Nat {
    match o {
        Some(Some(n)) => n,
        Some(None()) => 1,
        None() => 0,
    }
}
match parsed {
    Ok((token, cursor)) => token,
    Err(e) => fallback,
}

if let

Since 1.5

When only one case is interesting, if let avoids a two-arm match:

if let Some(n) = maybe_value {
    n + 1
} else {
    0
}

Conditions chain with &&, binding several patterns at once. All must match for the branch to be taken:

fn both(a: Result<Nat, String>, b: Result<Nat, String>) -> Nat {
    if let Ok(x) = a && let Ok(y) = b { x + y } else { 0 }
}

let else

Since 1.5

let else binds a refutable pattern in the common case and takes an escape route otherwise. The else block must diverge — it has to leave the enclosing function rather than produce a value:

fn or_default(o: Option<Nat>) -> Nat {
    let Some(n) = o else { return 0 };
    n
}

Note the shape: no semicolon after return 0 inside the block, and one after the closing brace. n is in scope for the rest of the function, which is the advantage over match — the happy path does not get indented.