Desestructuración
En esta página
La desestructuración enlaza varios nombres a la vez al comparar la forma de un array, struct, record o enum. El mismo lenguaje de patrones aparece en los puntos de binding y, cuando se indica, al asignar variables existentes.
Bindings posicionales
Un patrón de tupla extrae valores por posición en un let. También funciona
con una función que devuelve varios valores:
let (lo, hi) = min_max([...]) — desestructura el valor de retorno de una función en dos nombres.
// 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 en let
Usa let Point { x, y } = point cuando sabes que el valor es un Point.
campo: local renombra el binding y .. ignora campos no nombrados. Añade un
bloque else cuando el valor pueda no coincidir:
Patrones de struct directamente en let, renombrado, .. y la forma falible 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
Los patrones de struct también funcionan directamente en parámetros de
función, lambdas y bucles for. Un patrón nombrado implica el tipo del
parámetro; un patrón de record anónimo necesita una anotación explícita:
Patrones nombrados y anónimos en parámetros, closures y 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 y spread de struct
..rest captura los campos que no se nombraron en un record anónimo tipado. En
un literal de struct nombrado, ..source copia campos de otro valor; los campos
nombrados explícitamente tienen prioridad:
Rest records tipados, parámetros de record estructurales y 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
Asignación a variables existentes
Los patrones sin let asignan variables que ya existen. El lado derecho se
evalúa primero, por lo que (a, b) = (b, a) intercambia de forma segura. La
asignación de struct selecciona campos por nombre y _ descarta una posición:
Intercambio, paso de Fibonacci, desempaquetado de retorno, descarte y asignación nombrada 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
Patrones en match
El patrón Type { campo } enlaza campos en match e if let; .. ignora los
campos restantes y campo: local renombra durante la extracción:
Extrayendo x e y de Point; renombrando campos; ignorando el resto con ..
// 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)
La desestructuración de enum usa el mismo match, con la variante en
notación de punto (Enum.Variant(x)). Cada variante se convierte en una rama
separada; if let sirve para el camino rápido cuando solo importa una variante:
Result.Ok(v) y Result.Err(e) como ramas de match; if let para el camino 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
Desafío
Empieza con dos variables existentes y actualiza ambas con una sola asignación
desestructurante. Después reescribe la actualización como binding let y
explica por qué las dos formas no son intercambiables.
Consulta también