3.14. Tuple
Tuples are a concise syntax to create anonymous data structures.
A tuple type is declared with the tuple keyword followed by a list of element types
(optionally named) in angle brackets:
var unnamed : tuple<int; float> // unnamed elements
var named : tuple<i:int; f:float> // named elements
Tuple field names are part of the type. Two tuple declarations are the same only if they have the same number of elements, the same element types, and the same field names (in the same positions). An unnamed tuple is not assignable to a named tuple, and a named tuple is not assignable to a tuple with different names — even when the element types match:
var ta : tuple<int; float>
var tb : tuple<i:int; f:float>
var tc : tuple<x:int; y:float>
var td : tuple<i:int; f:float>
tb = td // ok — same names, same types
Both mismatched assignments are rejected:
var lhs : tuple<int; float>
var rhs : tuple<i:int; f:float>
lhs = rhs // error[30915]: tuple<int;float> is not the same type as tuple<i:int;f:float>
var lhs : tuple<i:int; f:float>
var rhs : tuple<x:int; y:float>
lhs = rhs // error[30915]: tuple<i:int;f:float> is not the same type as tuple<x:int;y:float>
The same rule applies to construction: a bare positional literal (1, 2.0)
produces an unnamed tuple<int;float> and is not accepted where a named
tuple type is expected. Use the named-field literal form to construct a named
tuple directly:
var named_ok : tuple<i:int; f:float> = (i = 1, f = 2.0) // ok
var named_bad : tuple<i:int; f:float> = (1, 2.0) // error[30344]: not the same type
Mixing named and positional fields in the same literal is not supported — either every field is named or none are.
3.14.1. Shorthand promotion
When every element of a positional tuple literal is a bare variable reference, the compiler may promote it to a named tuple by taking the field names from the variables. This only fires when the target type is unambiguously a named tuple and the variable names match the field names in order:
let eid = 7
let distSq = 2.5
var t : tuple<eid:int; distSq:float> = (eid, distSq) // ok, promoted
var arr : array<tuple<eid:int; distSq:float>>
arr |> push((eid, distSq)) // ok, promoted
def make_hit(eid : int; distSq : float) : tuple<eid:int; distSq:float> {
return (eid, distSq) // ok, promoted
}
Promotion is a fallback: if a matching overload already exists for the unnamed tuple type, that overload wins and no promotion happens. If you want the named overload, use the explicit named-field literal:
def overload_pick(hit : tuple<int; float>) { return 1 }
def overload_pick(hit : tuple<x:int; y:float>) { return 2 }
let x = 1
let y = 2.0
overload_pick((x, y)) // returns 1: unnamed overload wins
overload_pick((x=x, y=y)) // returns 2: explicit named literal
A name mismatch fails compilation rather than silently constructing the unnamed tuple:
let foo = 1
let bar = 1.1
var hits : array<tuple<eid:int; distSq:float>>
hits |> push((foo, bar)) // error[30341]: no matching functions or generics
Promotion does not fire when any element is not a bare variable reference, e.g.
(a, a+1) stays unnamed.
Tuple elements can be accessed via nameless fields, i.e. _ followed by the 0 base field index:
ta._0 = 1
ta._1 = 2.0
Named tuple elements can be accessed by name as well as via nameless field:
tb.i = 1 // same as _0
tb.f = 2.0 // same as _1
tb._1 = 2.0 // _1 is also available
Tuples follow the same alignment rules as structures (see Structures).
Tuple alias types can be constructed the same way as structures. For example:
tuple Foo {
a : int
b : float
}
It’s the same as:
typedef Foo = tuple<a:int; b:float>
Tuples can be constructed using the tuple constructor, for example:
var tup_a = (1,2.0,"3")
var tup_b = tuple(1, 2.0, "3")
The => operator creates a 2-element tuple from its left and right operands:
var pair = "one" => 1 // tuple<string;int>, same as tuple("one", 1)
This works in any expression context, not just table literals.
Table literals like { "one"=>1, "two"=>2 } use => to form key-value tuples
that are then inserted into the table (see Tables).
Tuple elements can be assigned names via tuple constructor:
var named3 = tuple<a:int; b:float; c:string>(a=1, b=2.0, c="3")
A tuple constructor that spells out the element types accepts named arguments only —
tuple<a:int; b:float>(a=1, b=2.0) is fine, tuple<int; float>(1, 2.0) is a syntax error.
Use the auto form tuple(1, 2.0) for positional construction.
Array of tuples can be constructed using similar syntax, with a comma as a separator:
typedef Tup = tuple<a:int; b:float; c:string>
let H : array<Tup> <- array<Tup>((a = 1, b = 2., c = "3"), (a = 4, b = 5., c = "6"))
Tuples can be expanded upon the variable declaration, for example:
var (first, second, third) = (1, 2.0, "3")
In this case only one variable is created, as well as for ‘assume’ expressions. I.e:
var first`second`third = (1, 2.0, "3")
assume first = first`second`third._0
assume second = first`second`third._1
assume third = first`second`third._2
Each destructured name binds like a let declaration: reusing a name that is
already an alias, local, argument, or an earlier destructured name is
error[30704], the same rule plain let follows. The one exception is
_, the discard — it binds nothing, so it can repeat freely, within one
pattern and across patterns:
let (_, _, only_third) = (1, 2.0, "3")
assert(only_third == "3")
Iterators and containers can be expanded in the for-loop in a similar way
(the _ discard works here too):
var H <- [(1, 2.0, "3"), (4, 5.0, "6")]
for ( (a, b, c) in H ) {
assert(a == 1)
assert(b == 2.0)
assert(c == "3")
}
3.14.1.1. Passing tuples as arguments — const widening
When a tuple value is passed as a function argument, each pointer field
inside the tuple participates in the same const-widening rule used for
top-level pointer parameters (see Pointers).
A non-const pointer T? inside the argument tuple is accepted where the
parameter tuple has T const?:
struct Loc { line : int }
struct Node { at : Loc }
// Exact-match overload.
def takeng(hits : array<tuple<string; Loc const?>>; hit : tuple<string; Loc const?>) { pass }
// Generic overload — TT is inferred from the array element type,
// so hit must match tuple<string; Loc const?> as well.
def take(hits : array<auto(TT)>; hit : TT) { pass }
def feed(var node : Node?&; ats : array<tuple<string; Loc const?>>) {
take(ats, ("test", unsafe(addr(node.at)))) // tuple<string; Loc?>
takeng(ats, ("test", unsafe(addr(node.at)))) // accepted for
// tuple<string; Loc const?>
}
The widening is one-directional (T? widens to T const?, not the
reverse) and applies to tuples that appear directly as an argument type.
Tuples nested inside containers or inside other structures are not relaxed —
their element types must match exactly. Variants and options do not
participate in this relaxation.
The implementation lives in TypeDecl::isSameType in
src/ast/ast_typedecl.cpp: the isPassType flag is propagated into the
tuple’s argTypes comparison so the pointer field inherits the same
relaxation that the top-level parameter pointer gets.
See also
Datatypes for a list of built-in types,
Pattern matching for matching and destructuring tuples,
Finalizers for tuple finalization,
Move, copy, and clone for tuple copy and move rules,
Aliases for the typedef shorthand tuple syntax,
Pointers for the matching rule on top-level pointer arguments.