Skip to content
Noodle
InstallLearnPlayground
GitHub

Records and tuples

Records and tuples are structural values built into the language. They are especially useful for short-lived data assembled inside one function. They do not require a datatype declaration and have no nominal identity.

An anonymous record is often the clearest representation for a temporary intermediate result. Keep it inside the function that creates and consumes it:

func distance_components(x : Int, y : Int) -> Int do
components = {horizontal: x, vertical: y};
components.horizontal * components.horizontal +
components.vertical * components.vertical
end

The fields document the local calculation without creating a public data-model name. This is a good fit for a short pipeline, a function-local accumulator, or a value that never crosses a module boundary.

Record types are exact. Two anonymous record types are equal when they contain the same field names with equal types; extra or missing fields are not compatible. Source field order does not matter.

You can give an anonymous record a short name while prototyping:

type Point = {x: Int, y: Int}
func translate(point : Point, dx : Int, dy : Int) -> Point do
{x: point.x + dx, y: point.y + dy}
end

Point is a transparent alias, not a new nominal type. Another alias or an inline record with the same exact fields has the same structural type. This is convenient during rapid development, but it does not communicate a distinct domain identity or prevent unrelated values with the same shape from being passed to the function.

When a record crosses several function boundaries, is shared between modules, appears in a public API, or represents a stable domain concept, replace the anonymous record alias with a single-constructor named-field datatype:

datatype Point
Point{x: Int, y: Int}
end
func translate(point : Point, dx : Int, dy : Int) -> Point do
Point::Point{x: point.x + dx, y: point.y + dy}
end
func main() -> Unit do
origin = Point::Point{x: 0, y: 0};
moved = translate(origin, 3, 4);
Debug.trace(moved.x);
Debug.trace(moved.y);
end

The Point datatype has nominal identity: a different datatype with the same fields is still a different type. Its constructor gives construction and pattern matching an explicit owner, and future fields or invariants can be introduced without pretending that every structurally equal record is the same domain value. See Datatypes and basic patterns for multiple constructors and exhaustive switch handling.

In short: use anonymous records for local, fast-moving code; treat a type Name = { ... } alias as a temporary structural convenience; and promote stable or shared data to a nominal named-field datatype.

When a local binding has the same name as a field, use a field pun:

type User = {name: String, active: Bool}
func make_user(name : String) -> User do
active = true;
{name, active}
end

A spread copies an existing record into a fresh immutable record. Later members replace fields supplied by the spread:

type User = {name: String, active: Bool}
func deactivate(user : User) -> User do
{..user, active: false}
end

The base expression is evaluated once. Record members are evaluated from left to right, even though the type system treats field order as irrelevant.

A tuple is positional structural data:

func divide_with_remainder(value : Int, divisor : Int) -> (Int, Int) do
(value / divisor, value % divisor)
end
func main() -> Unit do
result = divide_with_remainder(17, 5);
Debug.trace(result._0);
Debug.trace(result._1);
end

Tuple fields are _0, _1, and so on. A tuple type is equivalent to a record with those numbered fields. Tuple syntax requires at least two items:

  • () is the Unit value;
  • (value) only groups one expression;
  • (left, right) is a tuple.

Pattern bindings destructure structural data. They require an explicit root before @:

func ordered_pair(left : Int, right : Int) -> (Int, Int) do
if left <= right then (left, right) else (right, left) end
end
func main() -> Unit do
_ @ (lower, upper) = ordered_pair(9, 3);
Debug.trace(lower);
Debug.trace(upper);
end

The _ root discards the complete tuple after binding its items. A named root, such as whole @ (left, right), retains both the complete value and the projected fields. Pattern bindings must be exhaustive; a pattern that might fail belongs in a switch instead.

Next: datatypes and basic patterns.