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: oneContextual 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) // 10Map 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: 3Projection 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¶
DOCS / FEEDBACK
Did this page leave a question?
Tell us where the explanation lost you. Documentation is part of the language experience.