Skip to content

Guide 10 of 53

Records, maps and updates

Records, maps, projections and reusable field paths.

On this page

Records, maps, projections and reusable field paths.

Nested immutable updates

The with update evaluates its base once and copies the root and changed record prefixes. Unchanged children remain shared. The result preserves its nominal type; conflicting paths and linear resource copies are rejected.

Practice in By Example · Source program

// Item 4: copy only the record paths being changed, keeping the same type.
struct Address { city: str, postal_code: str }
struct User { name: str, address: Address, roles: [str] }

fn main() {
    let original = User {
        name: "João",
        address: Address { city: "São Paulo", postal_code: "01000-000" },
        roles: ["reader"],
    }
    let changed = original with { name: "Ana", address.city: "Recife" }
    print(original.name)
    // expected: João
    print(changed.name)
    // expected: Ana
    print(original.address.city)
    // expected: São Paulo
    print(changed.address.city)
    // expected: Recife
    print(changed.address.postal_code)
    // expected: 01000-000

    // The untouched roles reference is shared, while address was copied.
    changed.roles.push("reviewer")
    print(original.roles.len())
    // expected: 2
    let next = changed with { address.postal_code: "50000-000" } with { name: "Beatriz" }
    print(next.name)
    // expected: Beatriz
}

Explicit map types

Write a map type as #{K: V}. Braces with named fields always describe a record type, including fields named int or String. Empty braces are an empty record. Map<K, V> remains supported.

Practice in By Example · Source program

// Map types and map values use the same explicit delimiter.
let counts: #{str: int} = #{ ok: 2, failed: 1 }
print(counts["ok"])
// expected: 2

// Field labels never turn a record annotation into a map.
let row: { str: int, User: str, type: bool } = #{ str: 7, User: "Ada", type: true }
print(row.str)
// expected: 7
print(row.User)
// expected: Ada
print(row.type)
// expected: true

type Lookup = #{str: [#{int: str}]}
let nested: Lookup = #{ first: [#{ [1]: "one" }] }
print(nested["first"][0][1])
// expected: one

Contextual construction

When an exact struct type is expected, .{ fields } constructs that struct without repeating its name. Field defaults, privacy and completeness still apply. The compiler never guesses a nominal identity from field names.

Practice in By Example · Source program

// A known nominal context supplies the construction name.
struct Point { x: int = 0, y: int = 0 }
struct Segment { start: Point, end: Point }
fn origin() -> Point { .{} }
fn magnitude(point: Point) -> int { point.x + point.y }

fn main() {
    let x = 3
    let point: Point = .{ x, y: 4 }
    print(magnitude(.{ x: 5 })) // expected: 5
    print(point.y) // expected: 4
    let line: Segment = .{ start: .{ x: 1 }, end: .{ x: 7, y: 2 } }
    print(line.end.x) // expected: 7
    let shifted: Point = .{ x: point.x + 10, ..point }
    print(shifted.x) // expected: 13
}

Reusable typed field paths

field User::address.city produces a FieldPath<User, str> without reading an instance. get reads the leaf; set returns an immutable update and shares untouched children. Paths preserve privacy and exact leaf types; computed members and linear storage are rejected.

Practice in By Example · Source program

// Item 20: a typed field path can be reused and its getter is a callback.
struct Address { city: str, postal_code: str }
struct User { name: str, address: Address, roles: [str] }

fn city_path() -> FieldPath<User, str> = field User::address.city

fn main() {
    let city = city_path()
    let original = User {
        name: "Ana",
        address: Address { city: "São Paulo", postal_code: "01000-000" },
        roles: ["reader"],
    }
    let moved = city.set(original, "Recife")
    print(city.get(original))
    // expected: São Paulo
    print(city.get(moved))
    // expected: Recife
    print(moved.address.postal_code)
    // expected: 01000-000

    let cities = [original, moved].map(city.get)
    print(cities[0])
    // expected: São Paulo
    print(cities[1])
    // expected: Recife

    // The untouched collection is still shared by the immutable update.
    moved.roles.push("reviewer")
    print(original.roles.len())
    // expected: 2
}

Standalone anonymous records

Without a nominal target, .{ fields } infers an anonymous record with exact field types and ordinary field access. An exact expected struct still selects contextual nominal construction. An invalid supplied target is diagnosed instead of being silently replaced by a structural type.

Practice in By Example · Source program

// Anonymous records keep the exact type of each field.
// A nominal annotation still selects that type's ordinary constructor.
fn main() {
    let count = 3
    let summary = .{ count, total: 12 }
    let next = summary with { total: 15 }
    print(summary.count, summary.total)
    print(next.total)
    // expected: 3 12
    // expected: 15
}
main()

Computed immutable updates

A with update accepts compound operators such as value += 1. It reads the old field once and computes a replacement without mutating the original. Existing visibility, type, conflicting path and linear-copy restrictions still apply.

Practice in By Example · Source program

struct Counter { value: int, limit: int? }

let current = Counter { value: 2, limit: nil }
let next = current with { value += 1, limit ??= 10 }

print(current.value) // 2
print(next.value)    // 3
print(next.limit)    // 10

Map comprehensions

Build maps with for and if entries inside #{ }. Computed keys use [expression]: value. Iterables, keys and values evaluate once in source order; duplicate keys use ordinary map replacement behavior. Loop and filter bindings stay within their entry.

Practice in By Example · Source program

struct User { id: int, name: str, enabled: bool }

let users = [
    User { id: 1, name: "Ada", enabled: true },
    User { id: 2, name: "Lin", enabled: false },
]
let names = #{for user in users => [user.id]: user.name}
let active_names = #{for user in users => if user.enabled => [user.id]: user.name}

print(names[2])
print(active_names[1])

Typed record projection

user.{ id, name } evaluates user once and creates a fresh anonymous record with only the selected visible physical fields. Exact types survive projection. Duplicate selections, inaccessible or computed fields and forbidden linear copies are rejected.

Practice in By Example · Source program

// Typed projection selects visible physical fields into an anonymous record.
struct User { id: int, name: str, internal: str }
var acquired = 0
fn make() -> User { acquired += 1; User { id: acquired, name: "Ada", internal: "hidden" } }
let public_user = make().{ name, id }
print(acquired)
print(public_user.id)
print(public_user.name)
let updated = public_user with { id += 4 }
print(public_user.id)
print(updated.id)
let empty = make().{}
print(acquired)
struct Box<T> { value: T, id: int }
macro selected(value) { $value.{ value } }
let integer_box: Box<int> = Box { value: 7, id: 1 }
let text_box: Box<str> = Box { value: "text", id: 2 }
let integer = selected!(integer_box)
let text = selected!(text_box)
print(integer.value)
print(text.value)
fn first() -> int { let unused = ({ return 8; User { id: 0, name: "", internal: "" } }).{ id }; 0 }
print(first())
var total = 0
:outer for n in 0..3 {
    let value = ({ if n == 1 { continue :outer }; User { id: n, name: "", internal: "" } }).{ id }
    total += value.id
}
print(total)

Grouped nested updates

A nested group in a with update shares a path prefix: address { city: value, visits += 1 } updates address.city and address.visits. It preserves the usual copy behavior, privacy checks and conflict checks. Initializers execute in written order.

// Update several fields under the same path, leaving the original value intact.
struct Address { city: str, visits: int }
struct Person { name: str, address: Address }
let original = Person { name: "Ana", address: Address { city: "Recife", visits: 2 } }
let moved = original with {
    address { city: "Natal", visits += 1 },
}
print(original.address.city) // expect: Recife
print(moved.address.city) // expect: Natal
print(moved.address.visits) // expect: 3

Projection paths and aliases

value.{ field, nested.field as name } copies selected stored fields into an anonymous record. The base is evaluated once. Each path must be visible and follow the existing copy rules. Computed accessors cannot be projected, and output names must be unique.

// Prepare a public response without copying internal fields.
struct Address { city: str, delivery_code: int }
struct User { id: int, name: str, address: Address }
let user = User {
    id: 7,
    name: "Ada",
    address: Address { city: "Recife", delivery_code: 42 }
}
let response = user.{ id, name as display_name, address.city as city }
print("{response.id}: {response.display_name}, {response.city}")

Array patterns with a suffix

One ...rest may appear between fixed elements. Prefix elements count from the beginning; suffix elements count from the end. The rest is a fresh array and may be empty. The input must have room for both the prefix and suffix.

// Read a framed packet without copying the header into its payload.
let packet = [170, 4, 8, 12, 255]
match packet {
    [170, ...payload, 255] => {
        print("payload: {payload}")
        print("bytes: {payload.len()}")
    },
    _ => print("invalid frame")
}

Map patterns

Every named key must be present before its value is tested. Keys are string literals; values accept nested patterns. Extra entries are allowed, and ...options copies the unselected entries to a new map. A nil value removes an entry, so it counts as absent. A map pattern does not match an anonymous record.

// Decode a command and retain options that belong to the handler.
let command = #{"action": "rename", "name": "Ada", "mode": "safe"}
match command {
    #{"action": "rename", "name": name, ...options} => {
        print("rename to {name}")
        print("mode: {options["mode"]}")
    },
    _ => print("unsupported command")
}

Continue learning

Documentation · Practice in By Example

DOCS / FEEDBACK

Did this page leave a question?

Tell us where the explanation lost you. Documentation is part of the language experience.

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

enespt-br