Records, methods, and contracts
Records
A record groups named fields. It can also declare methods, which receive the
instance as this. Records are shared handles: assignment, arguments, and
returns preserve the identity of the same mutable record. this cannot be
reassigned, but its fields may be mutated. See
Values, copying, and mutation.
record Point {
x: int,
y: int,
fn magnitude_squared(): int {
return this.x * this.x + this.y * this.y;
}
}
p: Point = Point { x: 3, y: 4 };
print(p.magnitude_squared()); // 25
A static fn belongs to the record rather than an instance and is often used as
a constructor:
record Counter {
value: int,
static fn zero(): Counter { return Counter { value: 0 }; }
}
Derive
derive asks the compiler to generate common methods from the fields:
eqgeneratesequals(structural equality).copygeneratescopy(a shallow copy: the record is new, but nested arrays and records remain shared).debuggeneratesto_text(a debug rendering).
record Vec2 derive eq, copy {
x: int,
y: int,
}
The generated copy() creates a new outer record. Each field is copied using
normal Dune semantics, so nested arrays and records remain shared. See
Explicit copies.
The Display contract
A record is printable when it provides a to_text(): text method.
print(record) and format("{}", record) call it.
record Point {
x: int,
y: int,
fn to_text(): text {
return format("({}, {})", this.x, this.y);
}
}
print(Point { x: 1, y: 2 }); // (1, 2)
import display; provides a matching Display contract (so a record can declare
with display.Display) and a show(value) helper.
Contracts
A contract names a set of method signatures a record can promise to implement
with the with clause. This gives generic code a way to require behavior.
contract Display {
fn to_text(): text;
}
record Tag with Display {
name: text,
fn to_text(): text { return this.name; }
}
Operator overloading
When the left operand of +, -, *, or / is a record, the operator
dispatches to a conventionally-named method on that record:
| Operator | Method |
|---|---|
a + b | a.add(b) |
a - b | a.sub(b) |
a * b | a.mul(b) |
a / b | a.div(b) |
The method is resolved with the normal overload rules, so the right operand can be another record or a scalar, and the result type is whatever the method returns:
record Vec2 {
x: int,
y: int,
fn add(other: Vec2): Vec2 { return Vec2 { x: this.x + other.x, y: this.y + other.y }; }
fn mul(factor: int): Vec2 { return Vec2 { x: this.x * factor, y: this.y * factor }; }
}
a: Vec2 = Vec2 { x: 1, y: 2 };
b: Vec2 = Vec2 { x: 3, y: 4 };
sum: Vec2 = a + b; // Vec2 { x: 4, y: 6 }
scaled: Vec2 = a * 10; // Vec2 { x: 10, y: 20 }
This is how the matrix module's vectors and matrices get
natural arithmetic — v + w, v * scalar, and element-wise v * w all map to
the corresponding Vector/Matrix methods. Applying an operator to a record
that lacks the matching method is a compile-time error
(operator '+' is not defined for type 'Point'). Matrix multiplication stays
explicit via .matmul(...) / .dot(...) to avoid ambiguity with element-wise
*.
Visibility across modules
Record fields and methods are private across module boundaries unless the member
is marked export. See Modules.