Skip to main content

Destructuring

Verum allows patterns on the left-hand side of let bindings, assignments, compound assignments, and function parameters. This page is the full grammar and semantic reference; for the pattern forms themselves, see Patterns.

The shared property: any place a binding name is valid, a structural pattern is valid.

Destructuring let

Tuples

let (a, b) = (1, 2);
let (x, y, z) = get_xyz();

// Ignore components:
let (first, _, _, fourth) = four_tuple();

// Single-element tuple requires a trailing comma:
let (a,) = (5,);

Arrays and slices

let [head, ..tail] = list; // head : T, tail : &[T]
let [first, second, ..] = list; // drop the rest
let [.., last] = list; // take only the last
let [a, b, .., y, z] = list; // prefix and suffix

// Exact size — fails to compile if the length is known and wrong:
let [x, y, z]: [Int; 3] = fixed_three();

.. may appear once per pattern. ..tail binds the remaining slice; a bare .. discards.

Records

type Point is { x: Float, y: Float, z: Float };

let Point { x, y, z } = origin(); // bind same-named fields
let Point { x, y, .. } = origin(); // grab two, drop z
let Point { x: px, y: py, .. } = origin(); // rename while binding

Record destructuring requires the path to the type:

let Circle { center: Point { x, y, .. }, radius } = disc();

Variants

Variant destructuring combines record and tuple forms:

type Event is
| Click(pos: Point)
| Keypress { code: Int, modifiers: Modifiers }
| Tick;

let Event.Click(pos) = ev; // tuple variant
let Event.Keypress { code, modifiers } = ev; // record variant
let Event.Tick = ev; // unit variant

Destructuring assignment

Existing bindings on the left can be reassigned with the same patterns:

let mut a = 1;
let mut b = 2;

(a, b) = (b, a); // parallel swap

let mut xs = [0, 0, 0, 0];
[xs[0], xs[1]] = [42, 43]; // array-position assignment

Record destructuring assignment:

let mut cfg = Config.default();
Config { timeout, .. } = load_override(); // update timeout field

Compound assignment with destructuring

Compound operators apply component-wise:

let mut position = (0, 0);
(position.0, position.1) += (1, 2); // → (1, 2)

let mut scores = [10, 20, 30];
[scores[0], scores[1], scores[2]] *= 2; // → [20, 40, 60]

let else

When a destructuring may fail — a refutable pattern — use let else to force a diverging fallback:

let Some(user) = registry.find(id) else {
return Response.not_found();
};

let [first, ..rest] = items else {
panic("items must be non-empty");
};

// The `else` block must diverge (return, continue, break, panic, etc.)

After let else, the binding is in scope and of the narrowed type: user: User (not Maybe<User>).

If-let chains

Several refutable patterns can chain with && in if/while:

if let Some(a) = ra
&& let Some(b) = rb
&& a + b > threshold
{
process(a, b);
}

The bindings accumulate: a is in scope when evaluating b's pattern, and both are in scope in the body. An else branch sees none of them.

while let works the same way:

while let Some(line) = reader.next_line()
&& !line.starts_with("# ")
{
process(line);
}

Destructuring in for

Loop variables use the full pattern grammar:

for (key, value) in map {
print(f"{key}: {value}");
}

for User { id, email, .. } in users {
notify(id, email);
}

for (i, item) in items.enumerate() {
print(f"{i}: {item}");
}

Destructuring in function parameters

A parameter's pattern is any pattern:

fn manhattan((x, y): (Int, Int)) -> Int {
x.abs() + y.abs()
}

fn handle(Event.Keypress { code, .. }: &Event) {
if code == 27 { exit_app(); }
}

Patterns on parameters must be irrefutable — they must always match. A refutable pattern (e.g. a variant) in a parameter position is a compile error. Use match inside the body instead when you need a refutable shape.

Rest and ignore

ConstructIn tuples/arraysIn records
_ignore one slotignore one field
..ignore middleignore remaining
..namebind rest as slice— (not valid in records)
let [_, _, x, ..] = items; // ignore first two, bind third
let [a, _, b, _, c] = five_slot(); // skip even slots
let Point { x, .. } = pt; // records use .. to skip

Nested patterns

Patterns nest to arbitrary depth:

let Shape {
kind: Kind.Rectangle {
width,
height: Height { millimetres, .. },
},
position: (x, y),
} = shape;

Destructuring, assignment, and let else all support nested forms.

Attributes on patterns

Individual pattern bindings can carry attributes, for optimisation hints or documentation:

fn process(@unused x: Int, @must_use result: &mut Out) { ... }

let @cold (fallback, recovery) = disaster_path();

Grammar

destructuring_target = '(' , expression_list , ')' (* tuple *)
| '[' , expression_list , ']' (* array *)
| path , '{' , field_expr_list , '}' (* record *)
| '(' , destructuring_target , ')' ;

destructuring_assign = destructuring_target , assign_op , assignment_expr ;
let_stmt = 'let' , pattern , [ ':' , type_expr ] , [ '=' , expression ] , ';' ;
let_else_stmt = 'let' , pattern , '=' , expression , 'else' , block_expr ;

See also