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.
25.1 Extending a Tuple
Section titled “25.1 Extending a Tuple”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.
25.2 Extension in Generic Types
Section titled “25.2 Extension in Generic Types”The same operator works in types and generic functions:
function append<T, U>(left: T, right: U): T ~ U = left ~ rightThe 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.
25.3 Tuple Shape and Accessors
Section titled “25.3 Tuple Shape and Accessors”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 intactEach 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).initReconstructing 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")25.4 Pair and Recursive Implementations
Section titled “25.4 Pair and Recursive Implementations”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.
25.5 Parser Composition
Section titled “25.5 Parser Composition”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.
25.6 Enclosing Parameter Bounds
Section titled “25.6 Enclosing Parameter Bounds”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 ~ otherThis 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.
