Skip to content

Tuple Extension (Advanced)

Tuple extension lets generic code build tuples and define behavior across tuple arities. This chapter assumes familiarity with tuples, generics, and trait implementations.

Use binary ~ to append one element. A non-tuple left operand forms a pair; a tuple left operand grows by one element:

let pair = 1 ~ true // (1, true)
let triple = pair ~ "hello" // (1, true, "hello")
let nested = pair ~ ("a", "b") // (1, true, ("a", "b"))

The right operand always remains one element. Chaining associates to the left, so 1 ~ 2 ~ 3 produces (1, 2, 3). Both operands execute once, left to right.

~ binds below comparisons, so compare an extension result with parentheses: (1 ~ true) == (1, true). Prefix ~ still means bitwise negation.

The same operator works in types and generic functions:

function append<T, U>(left: T, right: U): T ~ U = left ~ right

The compiler determines the result shape from T; no bound or overload is required. With T = Int32 and U = Bool, the result is (Int32, Bool). With T = (Int32, Bool) and U = String, it is (Int32, Bool, String). While the outer shape of T is unknown, the compiler preserves T ~ U until substitution reveals that shape.

Ordinary pair types behave differently: (T, U) always has two outer elements. Substituting a tuple for T preserves it as the first element of that pair.

Infer extension operands from ordinary function arguments or supply explicit type arguments. Reverse inference from an extension result is currently supported only for the tuple-constrained form described below.

Tuple is a built-in constraint for actual tuples of at least two elements. Aliases to tuples satisfy it; records and nominal wrappers around tuples do not. It is usable in where bounds, and cannot be implemented, inherited, or used as a runtime interface type.

Every tuple has .init and .last:

(1, true).init // 1
(1, true).last // true
(1, true, "x").init // (1, true)
(1, true, "x").last // "x"
((1, true), "x").init // (1, true), the first element intact

Each accessor evaluates its receiver once. For a pair, .init returns the first element. For a larger tuple, it returns the prefix tuple. .last always returns the final element.

For an unknown T: Tuple, the compiler defers the accessor’s result type until it knows the shape. A tuple bound alone does not prove that .init returns a tuple, and it says nothing about the traits implemented by individual elements.

For an actual extension T ~ U, .init has type T and .last has type U, even without a bound on T:

function prefix<T, U>(left: T, right: U): T = (left ~ right).init

Reconstructing an arbitrary tuple with these accessors requires care. If a pair’s first element is itself a tuple, extension flattens that first element:

let pair = ((1, true), "x")
let preserved = (pair.init, pair.last) // ((1, true), "x")
let extended = pair.init ~ pair.last // (1, true, "x")

A trait can handle tuple arities with a pair case (A, B) and a recursive case T ~ U where T: Tuple. The tuple bound guarantees that the recursive case has at least three outer elements. The pair case matches exactly two, including pairs whose first element is another tuple.

This example counts outer elements:

trait Arity =
function arity(self): Int32
implement <A, B> Arity for (A, B) =
function arity(self: (A, B)): Int32 = 2
implement <T, U> Arity for T ~ U where T: Tuple, T: Arity =
function arity(self: T ~ U): Int32 = self.init.arity() + 1
Actual type Implementation Bindings
(Int32, Bool) Pair A = Int32, B = Bool
(Int32, Bool, String) Recursive T = (Int32, Bool), U = String
((Int32, Bool), String) Pair A = (Int32, Bool), B = String

Selection does not depend on declaration order or implementation priority. A concrete triple implementation of Arity would overlap the recursive case and is rejected. Ordinary user-trait bounds cannot establish that two implementations are disjoint; Tuple supplies a specific structural arity fact.

Supported symbolic implementation heads have exactly the form T ~ U, with both operands declared as implementation parameters and T: Tuple. Unconstrained or chained symbolic heads are not supported.

The prelude’s Equatable, Hashable, Default, Comparable, and Display use pair/recursive implementations. Equality stops at the first unequal element; hashing combines elements in order; defaults are constructed element by element. Ordering is lexicographic, and formatting preserves flat versus nested tuple punctuation.

In standard.parser, left.append(right) and left ++ right sequence parsers and extend their successful results. For example:

let parser = parseChar('(') ++ parseDigit() ++ parseChar(')')

Its successful result has type (Char, Char, Char). As with value extension, a tuple returned by the right parser remains one element.

zip keeps both results intact as a pair. Use zipLeft or zipRight when one result, such as a delimiter, should be discarded.

An individual method can require that an enclosing parameter is a tuple:

record TupleBox<T> = value: T
module TupleBox<T> =
function append<U>(self: TupleBox<T>, other: U): T ~ U where T: Tuple = self.value ~ other

This method returns a tuple of at least three elements. A TupleBox<Int32> can still be constructed, but its append method cannot be called because Int32 does not satisfy Tuple.

The bound is a requirement chosen by this method; the ~ operator itself continues to accept a scalar left operand. The method’s signature, body, and closures can all use its tuple proof. A generic caller must carry a matching proof, and taking a method reference checks the same requirement as a call. The advanced generics chapter explains the rules for modules, classes, implementations, and extension blocks.

For the compiler design and implementation milestones, see the tuple extension design.