Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Records

A record is a fixed set of named fields. You declare one with struct, build one with a brace literal, and read a field with a dot. There is no impl block and no method syntax: a record is data, and the operations on it are the functions you write.

struct Point {
    x: Int
    y: Int
}

var p = Point { x: 3, y: 4 }
out(p)
out(p.x)

// A field is an assignable place, in every spelling an assignment has.
p.x = 5
p.y += 1
out(p)

// Field punning: `x` and `y` are already the names the fields want.
var x = 10
var y = 20
out(Point { x, y })
{ x: 3, y: 4 }
3
{ x: 5, y: 5 }
{ x: 10, y: 20 }

Fields are separated by a comma or a line break, so struct Point { x: Int, y: Int } on one line and the four-line form above are the same declaration. A trailing comma closes the list.

Note what out prints: { x: 3, y: 4 }, with no Point in front of it. A record formats as its fields.

Fields are places, and a record is an object

p.x = 5 writes the field. The binding is not what is written — the object is — so the receiver need not be a name you reassign, and every other reference to that record sees the write.

struct Point { x: Int, y: Int }

// A record value is an object. A binding names it; it does not own it, so a
// write through one name is visible through every other.
var nodes = [Point { x: 0, y: 0 }]
nodes[0].x += 1
out(nodes)

var alias = nodes[0]
alias.y = 9
out(nodes[0])
[{ x: 1, y: 0 }]
{ x: 1, y: 9 }

nodes[0].x += 1 evaluates nodes[0] once, reads the field and writes it back. min= and max= are among the spellings a field accepts, and they mean on a field what they mean everywhere: keep the better of the two. A field is always present, so the “an absent entry accepts the first value” half — which is the Map half — has nothing to apply to; what is left is the comparison, and the field’s type has to have one.

Equality, hashing and nesting come for free

Two records of the same type are equal when their fields are equal, and they hash consistently with that, so a record is a map key or a set element with no declaration on your part. Records nest.

struct Point { x: Int, y: Int }
struct Segment { label: Text, from: Point, to: Point }

// Two records of the same type with equal fields are equal, and hash alike.
var a = Point { x: 1, y: 2 }
var b = Point { x: 1, y: 2 }
out(a == b)

var seen = Set()
seen.insert(a)
seen.insert(b)
out(seen.len())

var owner = Map()
owner[a] = "north"
out(owner[b])

// Records nest, and a nested field is read through the outer one.
var s = Segment { label: "edge", from: a, to: Point { x: 4, y: 6 } }
out(s)
out(s.to.y)
true
1
north
{ label: edge, from: { x: 1, y: 2 }, to: { x: 4, y: 6 } }
6

A struct is nominal: it is the same type as itself and nothing else, so two declarations with identical fields are two types. Point { x: 1, y: 2 } == Vector { x: 1, y: 2 } does not compare unequal — it is Y001, expected Point, found Vector.

You cannot derive or implement any of this, and there is nothing to opt out of: equality, hashing and formatting are decided by the compiler from the field types.

Records are not ordered. Nothing says which field decides, so < and sorted() refuse them:

struct Point { x: Int, y: Int }

// Records are equatable and hashable. They are not ordered: nothing says which
// field decides, so `<` and `sorted()` refuse them. Sort by a key instead.
var ps = [Point { x: 2, y: 0 }, Point { x: 1, y: 9 }]
out(ps.sorted())
$ praxis check docs/book/examples/records-enums/record-cannot-be-ordered.px
error[Y006]: values of type `Point` cannot be ordered

  record-cannot-be-ordered.px:6:8
  6 | out(ps.sorted())
    |        ^^^^^^ values of type `Point` cannot be ordered

praxis: 1 error(s)

Name the field that decides and it sorts:

struct Point { x: Int, y: Int }

var ps = [Point { x: 2, y: 0 }, Point { x: 1, y: 9 }]
out(ps.sorted_by_key(|p: Point| p.x))
[{ x: 1, y: 9 }, { x: 2, y: 0 }]

A record literal must not be mistaken for a block

if p { … } is genuinely ambiguous: p { … } is a well-formed record literal, and p followed by a block is a well-formed if. The rule is that four keyword heads — if and while’s conditions, for’s iterator, match’s scrutinee — claim the brace as their block, and everywhere else a literal is legal.

struct Point { x: Int, y: Int }

var p = Point { x: 1, y: 2 }

// `if`, `while`, `for` and `match` claim the brace that follows their head, so
// a record literal there needs parentheses to say it is not the block.
if (Point { x: 1, y: 2 } == p) {
    out("same")
}

// Inside a bracket the grammar knows what closes it, so a literal is legal at
// any depth: an argument list, a match arm body, a block, a closure.
out(match p {
    Point { x: 1, y } => Point { x: 9, y: y }
    _ => p
})
out([Point { x: 0, y: 0 }].map(|q: Point| Point { x: q.y, y: q.x }))
same
{ x: 9, y: 2 }
[{ x: 0, y: 0 }]

Suppression follows the head’s operands and stops at brackets: a parenthesized expression, an argument list, a block, a record body and a match arm all re-enter with literals allowed. Writing if p == Point { x: 1, y: 2 } { … } without the parentheses is not a tidy error — the { becomes the if’s block, the field list becomes statements, and one line yields a dozen diagnostics. The fix is the parentheses.

The anonymous literal has no such ambiguity to suppress, and none of this applies to it. p { … } is ambiguous because the name could be the whole head; a { where an operand is still required cannot be a keyword’s block, because that block comes after a complete head. So if { hit: true }.hit { … } needs no parentheses. What that form has instead is its own tie with the block — { x: 1 } versus { x } — which Records without names covers.

Anonymous records

A record literal with no name in front of it — { x: 1, y: 2 } — builds a record whose type is its field set, and a named-capture template derives the same kind with no declaration anywhere. Between them this is where most records in a puzzle-shaped program come from.

// A named-capture template derives a record type with no declaration anywhere.
var points = read lines(`{x:int},{y:int}`)
out(points)
out(points[0].x)

// The derived record is an ordinary value: its fields are read by name, and a
// function that takes one need not name the type.
fn area(r) -> Int { r.x * r.y }
for r in points {
    out(area(r))
}

Given this input:

3,4
10,20
[{ x: 3, y: 4 }, { x: 10, y: 20 }]
3
12
200

An anonymous record is a type of its own. It prints as { x: Int, y: Int }, but that is a spelling diagnostics use and not one you can write as an annotation: the type grammar has no record form, so var p: { x: Int, y: Int } does not parse and an anonymous record only ever gets its type from inference — including from a { x: 1, y: 2 } literal, which is the value form and does parse. Two anonymous records are the same type when their field names match and their field types unify. A struct is not one of them, however alike the two look.

struct Point { x: Int, y: Int }

// An anonymous record is a type of its own. It is the same type as another
// anonymous record with the same field names and types, and it is never the
// same type as a `struct` that happens to look like it.
var rows = read lines(`{x:int},{y:int}`)
var p: Point = rows[0]
out(p)
$ praxis check docs/book/examples/records-enums/record-anonymous-is-not-nominal.px
error[Y001]: expected Point, found { x: Int, y: Int }

  record-anonymous-is-not-nominal.px:7:16
  7 | var p: Point = rows[0]
    |                ^^^^^^^ expected Point, found { x: Int, y: Int }

praxis: 1 error(s)

To cross the line, build the struct from the fields: Point { x: rows[0].x, y: rows[0].y }. Most programs never need to — the anonymous record already has the fields, and a function that takes one need not name its type. The type system’s side of it is Records without names.

A bare .name is a field; a zero-argument accessor is a call

p.x reads a field: it lowers to a slot index taken from the record’s definition. v.len() calls a method: it looks the name up in the catalog. The two are different syntax on purpose, and there is no property form of a method.

struct Row { len: Int }

var r = Row { len: 4 }
out(r.len)

var v = [1, 2, 3]
out(v.len())
out(v.len)
$ praxis check docs/book/examples/records-enums/record-field-is-not-a-call.px
error[Y112]: no field `len` on type `Vec[Int]`

  record-field-is-not-a-call.px:8:7
  8 | out(v.len)
    |       ^^^ no field `len` on type `Vec[Int]`

praxis: 1 error(s)

A record field may be called len and is unaffected: r.len reads it, r.len() looks for a method. v.len(), grid.width() and grid.height() all take their parentheses, and a bare one of those is Y112 naming the type it was asked of. The rule exists because a receiver whose type inference has not pinned yet cannot tell a field read from a nullary call, and picking by “whichever the receiver happens to have” would mean adding a field could silently change what an existing expression does.

What the compiler reports

A literal must supply every field, exactly once, and only fields the record has.

struct Point { x: Int, y: Int }

var missing = Point { x: 1 }
var extra = Point { x: 1, y: 2, z: 3 }
var twice = Point { x: 1, x: 2, y: 3 }
out(missing.z)
$ praxis check docs/book/examples/records-enums/record-literal-fields.px
error[Y113]: `Point` literal is missing a field: y

  record-literal-fields.px:3:15
  3 | var missing = Point { x: 1 }
    |               ^^^^^^^^^^^^^^ `Point` literal is missing a field: y

error[Y114]: `Point` has no field `z`

  record-literal-fields.px:4:33
  4 | var extra = Point { x: 1, y: 2, z: 3 }
    |                                 ^ `Point` has no field `z`

error[Y115]: field `x` is initialized more than once

  record-literal-fields.px:5:27
  5 | var twice = Point { x: 1, x: 2, y: 3 }
    |                           ^ field `x` is initialized more than once

error[Y112]: no field `z` on type `Point`

  record-literal-fields.px:6:13
  6 | out(missing.z)
    |             ^ no field `z` on type `Point`

praxis: 4 error(s)

A type cannot refer to itself

There are no recursive types. A declaration that reaches itself through its own annotations is N006, and a mutual pair is named through the member that closes the cycle.

struct Node {
    next: Node
    value: Int
}

struct A { b: B }
struct B { a: A }

out(1)
$ praxis check docs/book/examples/records-enums/record-self-referring.px
error[N006]: `Node` refers to itself, and a self-referring type is not supported

  record-self-referring.px:1:8
  1 | struct Node {
    |        ^^^^ `Node` refers to itself, and a self-referring type is not supported

error[N006]: `A` refers to itself through `B`, and a self-referring type is not supported

  record-self-referring.px:6:8
  6 | struct A { b: B }
    |        ^ `A` refers to itself through `B`, and a self-referring type is not supported

error[N006]: `B` refers to itself through `A`, and a self-referring type is not supported

  record-self-referring.px:7:8
  7 | struct B { a: A }
    |        ^ `B` refers to itself through `A`, and a self-referring type is not supported

praxis: 3 error(s)

Indirection does not help: struct Node { children: Vec[Node] } is the same N006, reported at Node. The message says the feature is missing rather than that the values are impossible, and that wording is deliberate — every field holds a reference, so a tree is a perfectly ordinary runtime shape. What is absent is recursive types in the type system. Model the tree with node ids instead: a Map[Int, Vec[Int]] of children.

One report is emitted per cycle member, and a declaration that merely sits behind a cycle is not the mistake and is not reported — in struct C { a: A } above struct A { b: B } and struct B { a: A }, only A and B are named.

What is not here

There is no generic struct: struct Box[T] { … } does not parse, and Option[T] is the one generic definition in the language — an enum, built in. There are no defaulted fields, no positional construction, no visibility modifiers, and no traits or interfaces to implement.

Taking a record apart in a pattern — including without naming its type — is pattern matching.