Desestruturação
Nesta página
A desestruturação liga vários nomes de uma vez ao comparar a forma de um array, struct, record ou enum. A mesma linguagem de padrões aparece nos pontos de binding e, quando indicado, na atribuição a variáveis existentes.
Bindings posicionais
Um padrão de tupla extrai valores por posição em um let. Ele também funciona
com uma função que retorna vários valores:
let (lo, hi) = min_max([...]) — desestrutura o retorno de uma função em dois nomes.
// Feature: Array destructuring (tuple-pattern)
// Syntax: `let (a, b, c) = [v1, v2, v3]`
// When to use: extract multiple values in one line, simulate
// "tuples" on the value side (in Zolo, tuples are arrays).
// Destructure directly from a literal.
let (a, b, c) = [1, 2, 3]
print(a) // 1
print(b) // 2
print(c) // 3
// Heterogeneous types — works because arrays are dynamic.
let (name, age, active) = ["Alice", 30, true]
print(name) // Alice
print(age) // 30
print(active) // true
// Destructure from a function returning an array.
fn min_max(arr: [int]) -> [int] {
var lo = arr[0]
var hi = arr[0]
for x in arr {
if x < lo { lo = x }
if x > hi { hi = x }
}
return [lo, hi]
}
let (lo, hi) = min_max([3, 1, 4, 1, 5, 9, 2, 6])
print("lo={lo} hi={hi}") // lo=1 hi=9
// In loops — destructure each element.
let pairs = [[1, "one"], [2, "two"], [3, "three"]]
for pair in pairs {
let (n, label) = pair
print("{n}={label}")
}
// expected:
// 1=one
// 2=two
// 3=three
Bindings de struct em let
Use let Point { x, y } = point quando o valor é sabidamente um Point.
campo: local renomeia o binding e .. ignora campos não nomeados. Acrescente
um bloco else quando o valor puder não casar:
Padrões de struct diretamente em let, renomeação, .. e a forma falível let … else.
// Feature: Struct destructuring in `let` (guarded)
// Syntax: `let Type { field, other: renamed, .. } = expr`
// When to use: extract fields when you KNOW the value is that struct.
// If the value might not match, use `let ... else`, `if let`, or `match`.
// A non-matching value panics: "destructure failed: expected T, got ...".
// Note: pattern-`let` is statement-level (inside fn bodies), like let-else.
struct Point {
x: int,
y: int,
}
// Basic: bind by field name — no `else` needed when the type is known.
fn dist2(p: Point) -> int {
let Point { x, y } = p
return x * x + y * y
}
let p = Point { x: 3, y: 4 }
print("dist2={dist2(p)}")
// expected: dist2=25
// Rename with `field: new_name`, ignore the rest with `..`.
fn xof(p: Point) -> int {
let Point { x: px, .. } = p
return px
}
print("x={xof(p)}")
// expected: x=3
// Fallible source? Keep the `else` form — the else block must diverge.
fn describe(v) {
let Point { x, .. } = v else {
print("not a point")
return
}
print("point at x={x}")
}
describe(p)
describe(42)
// expected: point at x=3
// expected: not a point
Padrões de struct também funcionam diretamente em parâmetros de função,
lambdas e loops for. Um padrão nomeado implica o tipo do parâmetro; um padrão
de record anônimo precisa de anotação explícita:
Padrões nomeados e anônimos em parâmetros, closures e for.
// Feature: Struct patterns in binding positions
// Syntax: `fn f(Point { x, y })`, `|{ name }| ...`, `for Point { x } in ...`
// When to use: extract fields right where the value arrives — no boilerplate
// `let` at the top of the body. A named pattern IMPLIES the parameter type
// (`fn dist(Point { x, y })` types the param as Point); anonymous patterns
// in fn params need an annotation (`{ name, .. }: User`).
// A non-matching value panics: "destructure failed: expected T, got ...".
struct Point {
x: int,
y: int,
}
struct User {
name: str,
age: int,
}
// Function parameter — named pattern, type implied.
fn dist2(Point { x, y }) -> int {
return x * x + y * y
}
let p = Point { x: 3, y: 4 }
print("dist2={dist2(p)}")
// expected: dist2=25
// Anonymous pattern needs the annotation in fn position.
fn greet({ name, .. }: User) -> str {
return "hi " + name
}
print(greet(User { name: "Alice", age: 30 }))
// expected: hi Alice
// Closures — the sweet spot: |{ field }| in HOF chains.
fn names_of(us) {
return us.map(|{ name }| name)
}
let us = [User { name: "A", age: 1 }, User { name: "B", age: 2 }]
let ns = names_of(us)
print("{ns[0]}{ns[1]}")
// expected: AB
// for-in — destructure each element; a non-matching element panics.
fn sum_products(ps) -> int {
let mut acc = 0
for Point { x, y } in ps {
acc = acc + x * y
}
return acc
}
print("sum={sum_products([Point { x: 1, y: 2 }, Point { x: 3, y: 4 }])}")
// expected: sum=14
Rest records e spread de struct
..rest captura os campos que não foram nomeados em um record anônimo tipado.
Num literal de struct nomeada, ..source copia campos de outro valor; campos
nomeados explicitamente têm precedência:
Rest records tipados, parâmetros de record estruturais e Type { campo: valor, ..source }.
// Feature: `..rest` bindings, record types, and struct-literal spread
// Syntax:
// let Type { field, ..rest } = expr — rest = record with the remaining fields
// fn f(r: { name: str, age: int }) — anonymous record type annotation
// Type { field: v, ..source } — construction spread: named > spread > default
// When to use: split a struct into "the fields I want" + "everything else",
// pass field subsets without declaring a named struct, or copy-with-changes.
// Note: pattern-`let` is statement-level (inside fn bodies), like let-else.
struct User {
name: str,
age: int,
email: str,
}
// `..rest` collects the fields the pattern did not extract into an
// anonymous record — typed, so rest.age works and rest.name (extracted)
// is a compile-time error (TE102).
fn split(u: User) {
let User { name, ..rest } = u
print("name={name}")
print("rest.age={rest.age} rest.email={rest.email}")
}
let u = User { name: "Alice", age: 30, email: "alice@zolo.dev" }
split(u)
// expected: name=Alice
// expected: rest.age=30 rest.email=alice@zolo.dev
// Records are structural: any struct (or record) with AT LEAST those
// fields can be passed — no named type needed.
fn describe(r: { name: str, age: int }) -> str {
return "{r.name} ({r.age})"
}
print(describe(u))
// expected: Alice (30)
// Construction spread: start from an existing value, override some fields.
// Precedence: named field > spread value > declared default.
let renamed = User { name: "Bob", ..u }
print("renamed={renamed.name}/{renamed.age}/{renamed.email}")
// expected: renamed=Bob/30/alice@zolo.dev
// Pure copy-construct.
let copy = User { ..u }
print("copy={copy.name}")
// expected: copy=Alice
struct Point {
x: int,
y: int,
z: int = 0,
}
// Round trip: destructure with rest, rebuild with spread.
// `flat` holds { x, y }; the new literal overrides z and spreads the rest.
fn lift(p: Point) -> Point {
let Point { z, ..flat } = p
return Point { z: z + 10, ..flat }
}
let lifted = lift(Point { x: 1, y: 2 })
print("lifted={lifted.x},{lifted.y},{lifted.z}")
// expected: lifted=1,2,10
// The rest binding also works in anonymous patterns.
fn contact(u: User) {
let { email, ..basics } = u
print("email={email} basics.name={basics.name}")
}
contact(u)
// expected: email=alice@zolo.dev basics.name=Alice
Atribuição a variáveis existentes
Padrões sem let atribuem a variáveis que já existem. O lado direito é
avaliado primeiro, portanto (a, b) = (b, a) faz uma troca segura. A
atribuição de struct seleciona campos pelo nome e _ descarta uma posição:
Troca, passo de Fibonacci, desempacotamento de retorno, descarte e atribuição nomeada de struct.
// Feature: Destructuring assignment — re-assign EXISTING variables
// Syntax:
// (a, b) = expr — positional; RHS evaluated first (swap works)
// (a, _) = expr — `_` discards a slot
// Point { x, y: py } = p — by name; shorthand writes `x`, rename writes `py`
// When to use: update several existing variables at once — swaps, loop
// state steps, unpacking a returned pair into locals you already declared.
// This ASSIGNS (vars must exist, use `var`); to BIND new names, use
// `let (a, b) = ...` / `let Point { x } = ...` instead.
// A non-matching struct value panics: "destructure failed: expected T".
fn demo_swap() {
var a = 1
var b = 2
(a, b) = (b, a)
print("a={a} b={b}")
}
demo_swap()
// expected: a=2 b=1
// RHS-first means the right side reads the OLD values — one-line
// state steps without temporaries (here: a Fibonacci step).
fn demo_step() {
var x = 1
var y = 1
(x, y) = (y, x + y)
(x, y) = (y, x + y)
print("fib={y}")
}
demo_step()
// expected: fib=3
// Unpack a call result into existing vars; `_` discards.
fn minmax() -> (int, int) {
return (3, 9)
}
fn demo_unpack() {
var lo = 0
var hi = 0
(lo, hi) = minmax()
print("lo={lo} hi={hi}")
(lo, _) = (42, 99)
print("lo={lo}")
}
demo_unpack()
// expected: lo=3 hi=9
// expected: lo=42
struct Point {
x: int,
y: int,
}
// By name: shorthand `x` writes the var `x`; `x: px` writes `px`.
// Only the fields you NAME are assigned — no need to list them all.
fn demo_named(p: Point) {
var x = 0
var py = 0
Point { x } = p
Point { y: py } = p
print("x={x} py={py}")
}
demo_named(Point { x: 3, y: 4 })
// expected: x=3 py=4
// Fields and indexes are valid slots too (any assignable place).
fn demo_places() {
var p = Point { x: 0, y: 0 }
(p.x, p.y) = (5, 6)
print("p={p.x},{p.y}")
}
demo_places()
// expected: p=5,6
Padrões em match
O padrão Type { campo } liga campos em match e if let; .. ignora os
campos restantes e campo: local renomeia durante a extração:
Extraindo x e y de Point; renomeando campos; ignorando o restante com ..
// Feature: Struct destructuring via `match`
// Syntax: `match value { Type { field, other } => ... }`
// When to use: extract multiple fields from a struct in a single
// pattern. In Zolo, struct destructuring happens via `match`
// (or `if let`), not directly in `let`.
struct Point {
x: int,
y: int,
}
struct User {
name: str,
age: int,
email: str,
}
// Basic destructure — bindings have the same name as the fields.
let p = Point { x: 3, y: 4 }
match p {
Point { x, y } => print("x={x} y={y}"),
}
// expected: x=3 y=4
// Rename fields during destructure: `field: new_name`.
match p {
Point { x: px, y: py } => print("px={px} py={py}"),
}
// expected: px=3 py=4
// Ignore fields with `..` (rest pattern).
let u = User { name: "Alice", age: 30, email: "a@x.com" }
match u {
User { name, .. } => print("name={name}"),
}
// expected: name=Alice
// `if let` — destructure when the pattern matches.
if let User { email, .. } = u {
print("email={email}")
}
// expected: email=a@x.com
// Multiple fields extracted.
match u {
User { name, age, .. } => print("{name} ({age})"),
}
// expected: Alice (30)
A desestruturação de enum usa o mesmo match, com a variante em notação de
ponto (Enum.Variant(x)). Cada variante vira um braço separado; if let serve
para o caminho rápido quando só uma variante importa:
Result.Ok(v) e Result.Err(e) como braços de match; if let para o caminho feliz.
// Feature: Enum destructuring via `match` / `if let`
// Syntax: `match v { Enum.Variant(x) => ... }`
// When to use: extract the payload of an enum variant (Result.Ok,
// Option.Some, events with data, etc.). In Zolo, variants use
// dot-syntax: `MyEnum.Variant`, not `MyEnum::Variant`.
use std::Option
use std::Result
enum Result {
Ok(int),
Err(str),
}
enum Shape {
Circle(float),
Rectangle(float, float),
Point,
}
// Destructure each variant with its payload.
let r1 = Result.Ok(42)
let r2 = Result.Err("not found")
match r1 {
Result::Ok(v) => print("ok={v}"),
Result::Err(e) => print("err={e}"),
}
// expected: ok=42
match r2 {
Result::Ok(v) => print("ok={v}"),
Result::Err(e) => print("err={e}"),
}
// expected: err=not found
// Variants with multiple positional fields.
let s = Shape.Rectangle(10.0, 5.0)
match s {
Shape::Circle(r) => print("circle r={r}"),
Shape::Rectangle(w, h) => print("rect {w}x{h}"),
Shape::Point => print("point"),
}
// expected: rect 10x5
// `if let` — fast path for a single variant.
if let Result::Ok(v) = r1 {
print("got {v}")
}
// expected: got 42
// `if let` with `else` — useful when it does not match.
if let Result::Ok(v) = r2 {
print("got {v}")
} else {
print("no value")
}
// expected: no value
Desafio
Comece com duas variáveis existentes e atualize ambas com uma única atribuição
desestruturante. Depois reescreva a atualização como binding let e explique
por que as duas formas não são intercambiáveis.
Veja também