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.
Use anonymous records locally
Section titled “Use anonymous records locally”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.verticalendThe 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.
A type alias is still structural
Section titled “A type alias is still structural”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}endPoint 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.
Promote stable data to a nominal record
Section titled “Promote stable data to a nominal record”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);endThe 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.
Field puns and record spread
Section titled “Field puns and record spread”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}endA 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}endThe base expression is evaluated once. Record members are evaluated from left to right, even though the type system treats field order as irrelevant.
Tuples
Section titled “Tuples”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);endTuple 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 theUnitvalue;(value)only groups one expression;(left, right)is a tuple.
Exhaustive destructuring
Section titled “Exhaustive destructuring”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) endend
func main() -> Unit do _ @ (lower, upper) = ordered_pair(9, 3); Debug.trace(lower); Debug.trace(upper);endThe _ 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.