Reference: Records and Rows
The tutorial chapters use records one idea at a time. This page puts the whole machinery in one place: what kinds of record there are, how a function asks for "a record with at least these fields", how it abstracts over the rest of a record, and which of those pieces are limited today. The normative rules are in the spec; this page is the worked-example version.
Four things with fields
| written | has a name | row visible to bounds | can have methods | |
|---|---|---|---|---|
| anonymous record | { x = 1, y = 2 } | no | yes | no (aspect impls only, for a local aspect) |
record declaration | record Point { x: i64, … } | yes | yes | yes |
struct | struct Point { x: i64, … } | yes | no | yes |
| row variable | row R, used as ..R | — | stands for "the rest" | — |
The row of a type is its set of labelled fields. An anonymous record is its row.
A struct or record is a name (its brand) plus a row. The only difference between
struct and record is whether that row is visible to structural matching — that is
what the rest of this page is about.
Anonymous records
fun main() {
let p := { x = 1, y = 2 };
let q := p; // a record of Copy fields is copied, not moved
println(p.x + q.x);
println("${p}"); // a record of Display fields is Display
let c := { name = "ada", n = 1 }.clone(); // a record of Clone fields is Clone
println(c.name);
}
2
{ x: 1, y: 2 }
ada
Records are structural (same labels and field types ⇒ same type, in any order) and
exact (a { x, y } value is not a { x } value). Copy, Clone and Display hold for
a record exactly when they hold for every field. There is no ==, Eq, Ord or
Hash for records yet — see Known limits.
Named records
A record declaration is a struct whose row is public to structural matching. Every
field must be public:
record Point { x: i64, y: i64 }
fun x_of<record T: { x: i64, .. }>(v: T) -> i64 { v.x }
fun main() {
let p := Point { x = 1, y = 2 }; // a named record
let q := { x = 5, z = 9 }; // an anonymous record
println(x_of(p));
println(x_of(q));
}
1
5
Both satisfy the bound. Declaring the same type as a struct is rejected at the call —
and the error tells you the fix:
struct Point { x: i64, y: i64 }
fun x_of<record T: { x: i64, .. }>(v: T) -> i64 { v.x }
fun main() {
println(x_of(Point { x = 1, y = 2 }));
}
Turning a struct into a record is additive for callers (the type now satisfies
bounds it did not). Turning a record back into a struct is not: every caller
whose bound names the type's fields stops compiling, and nothing at the declaration
points at them. Choose record only when the field names really are the contract.
Row bounds
A bound on a record-kinded parameter says which labels the argument must carry:
fun user_of<record T: { user: i64, .. }>(e: T) -> i64 { e.user } // at least `user: i64`
fun has_user<record T: { user, .. }>(e: T) -> i64 { 1 } // an `user` of any type
fun no_token<record T: !{ token }>(e: T) -> i64 { 2 } // no `token` at all
fun any_record<record T>(e: T) -> i64 { 3 } // any record whatsoever
fun main() {
let ev := { user = 7, kind = "buy" };
println(user_of(ev));
println(has_user(ev));
println(no_token(ev));
println(any_record(ev));
}
7
1
2
3
Inside the body only the fields the bound lists are readable. { user: i64, .. } is
open (more fields allowed); { user: i64 } without the .. is closed (exactly
those). A pattern against an open bound must end in ...
Open rows: naming "the rest"
A row bound names which fields you need. A row variable names everything else, so the rest can appear in the signature:
fun get_x<row R>(p: { x: i64, ..R }) -> i64 { p.x }
fun keep<row R>(p: { x: i64, ..R }) -> { x: i64, ..R } { p } // the rest flows through
fun main() {
let wide := { x = 1, y = 20, flag = true };
println(get_x(wide));
let same := keep({ x = 2, tag = 7 });
println(same.tag);
}
1
7
row R is the binder; every use is written ..R. keep returns a record with the same
extra fields it was given — the caller keeps tag, with its type, though keep never
mentions it.
The by-value rule
Passing a wider record by value to a { x: i64, ..R } parameter hands over the
whole record. The parameter names x; the callee owns everything else R stands for
and may return it, destructure it, or let it drop at the end of its scope. The extra fields
need not be Copy:
fun get_x<row R>(p: { x: i64, ..R }) -> i64 { p.x }
fun main() {
let wide := { x = 1, name = "ada" }; // `name` is a String: not Copy, and that is fine
println(get_x(wide));
}
1
Borrow it instead and the caller keeps every field:
fun get_x<row R>(p: &{ x: i64, ..R }) -> i64 { p.x }
fun main() {
let wide := { x = 1, name = "ada" };
println(get_x(&wide));
println(wide.name); // still owned here
}
1
ada
Inside a generic body R is abstract, but the body owns it too, so it can pass the
parameter on by value with no bound. A field-wise constraint is
only needed to use the row more than once:
fun consume<row R>(p: { x: i64, ..R }) -> i64 { p.x }
fun outer<row R>(s: { x: i64, ..R }) -> i64 {
consume(s) + 1
}
fun main() {
println(outer({ x = 1, k = "kept", flag = true }));
}
2
Decomposing a row
A where equation splits a row variable into a named field and the remainder. It
implies the bound, and names the remainder (Rest) so a signature can mention it:
fun split<row R, row Rest>(p: { token: i64, ..R }) -> i64
where R = { extra: i64, ..Rest }
{
p.token + p.extra // `extra` is readable: the equation promised it
}
fun main() {
println(split({ token = 1, extra = 2, other = 3 }));
}
3
The equation also lets the body consume the named field and return its remainder:
fun strip<row R, row Rest>(p: { ..R }) -> { ..Rest }
where R = { token: String, ..Rest } {
let { token: _, ..rest } := p;
rest
}
fun main() {
println(strip({ token = "t", user = 3 }).user);
}
This prints 3. The discarded token is consumed; rest owns every other field.
The same pattern works inside a row-conditional impl, allowing a typestate method
to construct a result such as Session<..Rest>. With only token, the remainder is {}.
Rows on nominal types
A struct or record can take a row parameter, spliced as ..R in the type's own
argument list. That is how a type carries "whatever else the caller put in here":
record Builder<row R> { name: String, data: { ..R } }
fun describe<row R>(b: Builder<..R>) -> String { b.name }
fun auth_of<row R>(b: Builder<..R>) -> String where R = { auth: String, .. } {
b.data.auth
}
fun main() {
let b := Builder { name = "req", data = { auth = "tok", retries = 3 } };
println(describe(b));
println(auth_of(b));
let bare := Builder { name = "bare", data = { retries = 1 } };
println(describe(bare)); // fine: `describe` asks for nothing
// auth_of(bare) // rejected: no `auth` field (T0012)
}
req
tok
bare
Methods that exist only for some rows
An extend block may carry a row condition, and its methods exist only for receivers
whose row meets it. Disjoint conditions may reuse a method name:
record Session<row R> { id: i64, data: { ..R } }
extend<row R: { token: String, .. }> Session<..R> {
fun send(self) -> i64 { self.id * 10 } // only once a token is present
}
extend<row R: !{ token }> Session<..R> {
fun login(self) -> i64 { self.id } // only while there is none
}
fun main() {
let authed: Session<{ token: String }> := Session { id = 1, data = { token = "t" } };
let anon: Session<{ user: i64 }> := Session { id = 2, data = { user = 1 } };
println(authed.send());
println(anon.login());
// anon.send() // rejected: `anon`'s row has no `token`
}
10
2
The row is part of the type, so "has a token yet?" is checked at compile time — a state machine whose states are rows.
Field-wise constraints
A row bound constrains which labels exist. A field-wise constraint, where all R: Aspect, constrains what every field's type can do. It holds when each field of R
satisfies Aspect, and vacuously for an empty row:
aspect Mark { fun mark(&self) -> i64; }
extend i64: Mark { fun mark(&self) -> i64 { 1 } }
extend f64: Mark { fun mark(&self) -> i64 { 2 } }
aspect Label { fun label(&self) -> String; }
extend<row R> { ..R }: Label where all R: Mark {
fun label(&self) -> String { "all marked".to_string() }
}
fun main() {
println({ a = 1, b = 2.0 }.label());
println({ a = 1 }.label());
}
all marked
all marked
This is how the standard library gives Display and Copy to every record of
Display / Copy fields. A record with a field that lacks the aspect is T0012,
naming the field.
Implementing an aspect for a record
An aspect local to your module may be implemented three ways, from narrowest to widest:
// extend { x: f64, y: f64 }: MyAspect { … } one concrete row
// extend<row R: { x: f64, .. }> { ..R }: MyAspect { … } every row with an `x: f64`
// extend<row R> { ..R }: MyAspect { … } every row
Two row-conditional impls of the same aspect must be provably disjoint — some label
that one requires and the other forbids. Overlapping ones are T0015:
aspect Describe { fun describe(&self) -> String; }
extend<row R: { name: String, .. }> { ..R }: Describe {
fun describe(&self) -> String { "named ${self.name}" }
}
extend<row R: !{ name }> { ..R }: Describe {
fun describe(&self) -> String { "anonymous" }
}
fun main() {
println({ name = "ada", age = 36 }.describe());
println({ age = 36 }.describe());
}
named ada
anonymous
A record's current row decides which impl applies, so a partial move changes the
answer. Moving name out of a Handle leaves a value that no longer has one:
record Handle { id: i64, name: String }
aspect Describe { fun state(&self) -> String; }
extend<row R: !{ name }> { ..R }: Describe {
fun state(&self) -> String { "no name" }
}
extend<row R: { name: String, .. }> { ..R }: Describe {
fun state(&self) -> String { "has name" }
}
fun main() {
var h := Handle { id = 1, name = "secret" };
println(h.state());
let name := h.name; // moves `name` out; h : Handle.{ id }
println(h.state());
println(name);
}
has name
no name
secret
If a type has its own impl of the aspect, that impl wins over any row-conditional one.
Narrowing and widening
Moving a field out of a value narrows its type, for anonymous records and for structs
and records alike. Reading a moved-out field is a compile error:
fun main() {
let r := { left = "l", right = "r" };
let l := r.left; // moves `left`; r : { right: String }
println(r.right);
println(r.left); // error: no field `left` on { right: String }
}
A struct or record can also be projected (h.{ id } has type Handle.{ id })
and widened back by reassigning the moved field; an anonymous record narrows but
does not widen. See Move a slice, don't copy it for both.
Known limits
- Records have no
==,Eq,OrdorHash.Clone,CopyandDisplayexist (field-wise); the rest are separate designs.DisplayandClonefor records are built into the interpreter today (LIMIT-TYPES-003). - A deeper move does not narrow the outer value. Moving
root.a.bleavesroot's type whole; a record-typed field moves as a unit (GAP-OWNERSHIP-002). - Compile-time iteration over a row's fields (to write your own
Display-like aspect) is designed in RFC-0174 but not implemented.
Quick error guide
| code | what happened |
|---|---|
T0012 | a struct was given to a row bound; a record lacks a required label; a field lacks the aspect where all R: … needs |
T0015 | two row-conditional impls of one aspect overlap |
T0032 | an anonymous row (Builder<..>) or a fixed-field open record used where it is not accepted |
T0033 | a record spread through a reference needs every field Copy; clone it or spread an owned record |
T0036 | a by-value open-row parameter over a Drop type would drop a narrowed residual (not implemented yet) |
T0034 | one method name defined twice where both definitions can apply to the same receiver |
T0035 | a generic body used a method or field its declared bounds do not grant |
T0003 | no such field (moved out, or not in the bound), or an undeclared row variable |
Every code has a full entry in the error code reference.