Records
Records give names and structure to data. A structstruct defines one product of its fields; an enumenum defines a choice between variants. Both are nominal: two records with identical fields remain different types when their declared names are different.
A named struct lists each field and its type:
struct [value] Point { x: f64, y: f64}
struct [value] Point { x: f64, y: f64}
Construct a value by naming the record and supplying every field. Read a field with ..:
fn origin() -> Point { Point { x: 0.0, y: 0.0 }}fn length_squared(point: Point) -> f64 { point.x * point.x + point.y * point.y}
fn origin() -> Point { Point { x: 0.0, y: 0.0 }}fn length_squared(point: Point) -> f64 { point.x * point.x + point.y * point.y}
Constructor fields are matched by name, not by their written order. When an in-scope variable has the same name as a field, its name can be written once:
fn point(x: f64, y: f64) -> Point { Point { x, y }}fn point_on_x_axis(x: f64) -> Point { Point { y: 0.0, x }}
fn point(x: f64, y: f64) -> Point { Point { x, y }}fn point_on_x_axis(x: f64) -> Point { Point { y: 0.0, x }}
Point { x, y }Point { x, y } is shorthand for Point { x: x, y: y }Point { x: x, y: y }. Mixing shorthand and explicit fields is allowed, and the compiler still checks that every field is present exactly once with the declared type.
Records can be generic:
struct [value] Pair<A, B> { first: A, second: B}fn tagged<T>(tag: i64, value: T) -> Pair<i64, T> { Pair { first: tag, second: value }}
struct [value] Pair<A, B> { first: A, second: B}fn tagged<T>(tag: i64, value: T) -> Pair<i64, T> { Pair { first: tag, second: value }}
Type arguments are written between << and >>. Constructor calls usually infer them from the supplied fields and the expected return type.
An enum lists alternative constructors. A variant may have positional payload types or no payload at all:
enum [value] Option<T> { Some(T), None}fn present(value: i64) -> Option<i64> { Option::Some { value }}fn absent() -> Option<i64> { Option::None {}}
enum [value] Option<T> { Some(T), None}fn present(value: i64) -> Option<i64> { Option::Some { value }}fn absent() -> Option<i64> { Option::None {}}
The enum name qualifies each constructor. Constructor arguments use braces, even though variant payloads are declared with parentheses. The Control Flow chapter shows how matchmatch selects a variant and binds its payload.
Every record declaration chooses how instances are stored and managed. The three declaration capabilities are sharedshared, valuevalue, and regionalregional; omitting the annotation means sharedshared.
record capability├── shared├── value└── regional ├── flex └── rigid
record capability├── shared├── value└── regional ├── flex └── rigid
flexflex and rigidrigid refine a use of a regional record. They are not additional record-declaration capabilities: declare struct [regional] Nodestruct [regional] Node, then write [flex] Node[flex] Node only where a type needs the mutable regional view.
| Capability | Declaration | Meaning |
|---|---|---|
sharedshared |
struct Item { ... }struct Item { ... } |
The default. Instances live in reference-counted storage and can be shared by persistent functional values. |
valuevalue |
struct [value] Item { ... }struct [value] Item { ... } |
Instances are stored inline as ordinary values. This is suitable for small, non-recursive aggregates. |
regionalregional |
struct [regional] Item { ... }struct [regional] Item { ... } |
Instances are created inside a region. A fresh instance is flexflex; the value becomes rigidrigid when it is frozen at the region boundary. |
A sharedshared record is the general-purpose choice, so this declaration:
struct Box<T> { value: T}
struct Box<T> { value: T}
is equivalent to struct [shared] Box<T>struct [shared] Box<T>. Shared records are managed by reference counting. A [value][value] record is laid out inline instead; it may contain shared values, but a chain of value records cannot recursively contain itself because that would have no finite size.
Regional records add a lifecycle to the same nominal type:
- a flex value is a live, region-local view whose
[field][field]links can be changed; - a rigid value is the frozen view that can leave the region and be used like immutable data.
The regional rules are deliberately summarized here. The next chapter, Regional Mutability, will develop the region lifecycle and mutation rules separately.
Only a regional struct may declare a [field][field] member:
struct [regional] Node { value: i64, next: [field] Node}
struct [regional] Node { value: i64, next: [field] Node}
Although the declaration names NodeNode, the stored field type is Nullable<Node>Nullable<Node>. The implicit NullableNullable is important: a newly constructed node needs a valid empty link before there is another regional object to point to.
regional fn singleton(value: i64) -> [flex] Node { Node { value, next: Nullable::Null {} }}
regional fn singleton(value: i64) -> [flex] Node { Node { value, next: Nullable::Null {} }}
Assign through ->-> while the base is flex, and wrap the destination in the nonnull constructor explicitly:
regional fn connect(left: [flex] Node, right: [flex] Node) { left->next := Nullable::NonNull { right };}
regional fn connect(left: [flex] Node, right: [flex] Node) { left->next := Nullable::NonNull { right };}
Reading uses ordinary projection, so node.nextnode.next has a nullable type and must account for both cases:
fn next_value(node: Node, fallback: i64) -> i64 { match node.next { Nullable::NonNull(next) => next.value, Nullable::Null => fallback }}
fn next_value(node: Node, fallback: i64) -> i64 { match node.next { Nullable::NonNull(next) => next.value, Nullable::Null => fallback }}
A [field][field] link must point to another regional record. Its view follows the base: through a flex record it is a writable regional link; through a rigid record it is frozen. Flex values remain local to their active region, while rigid values may escape. Those constraints are enough to understand record declarations; their operational details belong in Regional Mutability.
Use [value][value] for compact scalar aggregates such as coordinates and small tags. Use the default [shared][shared] for persistent structures and ordinary heap-managed application data. Choose [regional][regional] when a construction phase needs local links or cycles that will be frozen before the result enters the rest of the program.