Tuples¶
A tuple is an ordered set of components that allows for the specification of a light-weight collection of values. As the examples in this chapter illustrate, tuples are a boon to the Chapel programmer. In addition to making it easy to return multiple values from a function, tuples help to support multidimensional indices, to group arguments to functions, and to specify mathematical concepts.
Tuple Types¶
A tuple type is defined by a fixed number (a compile-time constant) of component types. It can be specified by a parenthesized, comma-separated list of types. The number of types in the list defines the size of the tuple; the types themselves specify the component types.
The syntax of a tuple type is given by:
tuple-type:
( type-expression , type-list )
( type-expression , )
type-list:
type-expression
type-expression , type-list
A homogeneous tuple is a special-case of a general tuple where the types of the components are identical. Homogeneous tuples have fewer restrictions for how they can be indexed (Tuple Indexing). Homogeneous tuple types can be defined using the above syntax, or they can be defined as a product of an integral parameter (a compile-time constant integer) and a type.
Rationale.
Homogeneous tuples require the size to be specified as a parameter (a compile-time constant). This avoids any overhead associated with storing the runtime size in the tuple. It also avoids the question as to whether a non-parameter size should be part of the type of the tuple. If a programmer requires a non-parameter value to define a data structure, an array may be a better choice.
Example (homogeneous.chpl).
The statement
var x1: (string, real), x2: (int, int, int), x3: 3*int;defines three variables. Variable
x1
is a 2-tuple with component typesstring
andreal
. Variablesx2
andx3
are homogeneous 3-tuples with component typeint
. The types ofx2
andx3
are identical even though they are specified in different ways.
Note that if a single type is delimited by parentheses, the parentheses
only impact precedence. Thus (int)
is equivalent to int
.
Nevertheless, tuple types with a single component type are legal and
useful. One way to specify a 1-tuple is to use the overloaded *
operator since every 1-tuple is trivially a homogeneous tuple.
Rationale.
Like parentheses around expressions, parentheses around types are necessary for grouping in order to avoid the default precedence of the grammar. Thus it is not the case that we would always want to create a tuple. The type
3*(3*int)
specifies a 3-tuple of 3-tuples of integers rather than a 3-tuple of 1-tuples of 3-tuples of integers. The type3*3*int
, on the other hand, specifies a 9-tuple of integers.
Tuple Values¶
A value of a tuple type attaches a value to each component type. Tuple values can be specified by a parenthesized, comma-separated list of expressions. The number of expressions in the list defines the size of the tuple; the types of these expressions specify the component types of the tuple. A trailing comma is allowed.
The syntax of a tuple expression is given by:
tuple-expression:
( tuple-component , )
( tuple-component , tuple-component-list )
( tuple-component , tuple-component-list , )
tuple-component:
expression
'_'
tuple-component-list:
tuple-component
tuple-component , tuple-component-list
An underscore can be used to omit components when splitting a tuple (see Splitting a Tuple with Assignment).
Example (values.chpl).
The statement
var x1: (string, real) = ("hello", 3.14), x2: (int, int, int) = (1, 2, 3), x3: 3*int = (4, 5, 6);defines three tuple variables. Variable
x1
is a 2-tuple with component typesstring
andreal
. It is initialized such that the two components are"hello"
and3.14
, respectively. Variablesx2
andx3
are homogeneous 3-tuples with component typeint
. Their initialization expressions specify 3-tuples of integers.
Note that if a single expression is delimited by parentheses, the
parentheses only impact precedence. Thus (1)
is equivalent to 1
.
To specify a 1-tuple, use the form with the trailing comma (1,)
.
Example (onetuple.chpl).
The statement
var x: 1*int = (7,);creates a 1-tuple of integers storing the value 7.
Tuple expressions are evaluated similarly to function calls where the arguments are all generic with no explicit intent. So a tuple expression containing an array does not copy the array.
When a tuple is passed as an argument to a function, it is passed as if it is a record type containing fields of the same type and in the same order as in the tuple.
Tuple Indexing¶
A tuple component may be accessed by an integral parameter (a
compile-time constant) as if the tuple were an array. Indexing is
0-based, so the first component in the tuple is accessed by the index
0
, and so forth.
Example (access.chpl).
The loop
var myTuple = (1, 2.0, "three"); for param i in 0..2 do writeln(myTuple(i));uses a param loop to output the components of a tuple.
Homogeneous tuples may be accessed by integral values that are not necessarily compile-time constants.
Example (access-homogeneous.chpl).
The loop
var myHTuple = (1, 2, 3); for i in 0..2 do writeln(myHTuple(i));uses a serial loop to output the components of a homogeneous tuple. Since the index is not a compile-time constant, this would result in an error were tuple not homogeneous.
Rationale.
Non-homogeneous tuples can only be accessed by compile-time constants since the type of an expression must be statically known.
Iteration over Tuples¶
Homogeneous tuples support iteration via standard for
, forall
and coforall
loops. These loops iterate over all of the tuple’s
elements. A loop of the form:
[for|forall|coforall] e in t do
...e...
where t is a homogeneous tuple of size n
, is semantically equivalent
to:
[for|forall|coforall] i in 0..n-1 do
...t(i)...
The iterator variable for a tuple iteration is a either a const value or a reference to the tuple element type, following default intent semantics.
Heterogeneous tuples support iteration via standard for
and
coforall
loops. These loops iterate over all of the tuple’s
elements, giving each iteration its own index variable that is a
const ref
to the tuple element (note: this may change in the
future to include const
or ref
index variables). Thus, a
loop of the form:
for e in t do
...e...
where t is a heterogeneous tuple of size n
is semantically
equivalent to:
{ // iteration 0
const ref e = t(0);
...e...
}
{ // iteration 1
const ref e = t(1);
...e...
}
...
{ // iteration n-1
const ref e = t(n-1);
...e...
}
Similarly, a coforall loop is equivalent to the cobegin statement whose body is the series of compound statements from the serial case.
Tuple Assignment¶
In tuple assignment, the components of the tuple on the left-hand side of the assignment operator are each assigned the components of the tuple on the right-hand side of the assignment. These assignments occur in component order (component zero followed by component one, etc.).
Tuple Destructuring¶
Tuples can be split into their components in the following ways:
In assignment where multiple expression on the left-hand side of the assignment operator are grouped using tuple notation.
In variable declarations where multiple variables in a declaration are grouped using tuple notation.
In for, forall, and coforall loops (statements and expressions) where multiple indices in a loop are grouped using tuple notation.
In function calls where multiple formal arguments in a function declaration are grouped using tuple notation.
In an expression context that accepts a comma-separated list of expressions where a tuple expression is expanded in place using the tuple expansion expression.
Splitting a Tuple with Assignment¶
When multiple expression on the left-hand side of an assignment operator are grouped using tuple notation, the tuple on the right-hand side is split into its components. The number of grouped expressions must be equal to the size of the tuple on the right-hand side. In addition to the usual assignment evaluation order of left to right, the assignment is evaluated in component order.
Example (splitting.chpl).
The code
var a, b, c: int; (a, (b, c)) = (1, (2, 3));defines three integer variables
a
,b
, andc
. The second line then splits the tuple(1, (2, 3))
such that1
is assigned toa
,2
is assigned tob
, and3
is assigned toc
.
Example (aliasing.chpl).
The code
var A = [i in 1..4] i; writeln(A); (A(1..2), A(3..4)) = (A(3..4), A(1..2)); writeln(A);creates a non-distributed, one-dimensional array containing the four integers from
1
to4
. Line 2 outputs1 2 3 4
. Line 3 does what appears to be a swap of array slices. However, because the tuple is created with array aliases (like a function call), the assignment to the second component uses the values just overwritten in the assignment to the first component. Line 4 outputs3 4 3 4
.
When splitting a tuple with assignment, the underscore token can be used to omit storing some of the components. In this case, the full expression on the right-hand side of the assignment operator is evaluated, but the omitted values will not be assigned to anything.
Example (omit-component.chpl).
The code
proc f() do return (1, 2); var x: int; (x,_) = f();defines a function that returns a 2-tuple, declares an integer variable
x
, calls the function, assigns the first component in the returned tuple tox
, and ignores the other component in the returned tuple. The value ofx
becomes1
.
Splitting a Tuple in a Declaration¶
When multiple variables in a declaration are grouped using tuple notation, the tuple initialization expression is split into its type and/or value components. The number of grouped variables must be equal to the size of the tuple initialization expression. The variables are initialized in component order.
The syntax of grouped variable declarations is defined in Variable Declarations.
Example (decl.chpl).
The code
var (a, (b, c)) = (1, (2, 3));defines three integer variables
a
,b
, andc
. It splits the tuple(1, (2, 3))
such that1
initializesa
,2
initializesb
, and3
initializesc
.
Grouping variable declarations using tuple notation allows a 1-tuple to be destructured by enclosing a single variable declaration in parentheses.
Example (onetuple-destruct.chpl).
The code
var (a) = (1, );initialize the new variable
a
to 1.
When splitting a tuple into multiple variable declarations, the underscore token may be used to omit components of the tuple rather than declaring a new variable for them. In this case, no variables are defined for the omitted components.
Example (omit-component-decl.chpl).
The code
proc f() do return (1, 2); var (x,_) = f();defines a function that returns a 2-tuple, calls the function, declares and initializes variable
x
to the first component in the returned tuple, and ignores the other component in the returned tuple. The value ofx
is initialized to1
.
Splitting a Tuple into Multiple Indices of a Loop¶
When multiple indices in a loop are grouped using tuple notation, the tuple returned by the iterator (Iterators) is split across the index tuple’s components. The number of indices in the index tuple must equal the size of the tuple returned by the iterator.
Example (indices.chpl).
The code
iter bar() { yield (1, 1); yield (2, 2); } for (i,j) in bar() do writeln(i+j);defines a simple iterator that yields two 2-tuples before completing. The for-loop uses a tuple notation to group two indices that take their values from the iterator.
When a tuple is split across an index tuple, indices in the index tuple (left-hand side) may be omitted using underscores. In this case, no indices are defined for the omitted components.
However even when indices are omitted, the iterator is evaluated as if an index were defined. Execution proceeds as if the omitted indices are present but invisible. This means that the loop body controlled by the iterator may be executed multiple times with the same set of (visible) indices.
Splitting a Tuple into Multiple Formal Arguments in a Function Call¶
When multiple formal arguments in a function declaration are grouped using tuple notation, the actual expression is split into its components during a function call. The number of grouped formal arguments must be equal to the size of the actual tuple expression. The actual arguments are passed in component order to the formal arguments.
The syntax of grouped formal arguments is defined in Procedure Definitions.
Example (formals.chpl).
The function
proc f(x: int, (y, z): (int, int)) { // body }is defined to take an integer value and a 2-tuple of integer values. The 2-tuple is split when the function is called into two formals. A call may look like the following:
f(1, (2, 3));
An implicit where
clause is created when arguments are grouped using
tuple notation, to ensure that the function is called with an actual
tuple of the correct size. Arguments grouped in tuples may be nested
arbitrarily. Functions with arguments grouped into tuples may not be
called using named-argument passing on the tuple-grouped arguments.
In addition, tuple-grouped arguments may not be specified individually with
types or default values (only in aggregate). They may not be specified
with any qualifier appearing before the group of arguments (or
individual arguments) such as inout
or type
. They may not be
followed by ...
to indicate that there are a variable number of
them.
Example (implicit-where.chpl).
The function
f
defined asproc f((x, (y, z))) { writeln((x, y, z)); }is equivalent to the function
g
defined asproc g(t) where isTuple(t) && t.size == 2 && isTuple(t(1)) && t(1).size == 2 { writeln((t(0), t(1)(0), t(1)(1))); }except without the definition of the argument name
t
.
Grouping formal arguments using tuple notation allows a 1-tuple to be destructured by enclosing a single formal argument in parentheses.
Example (grouping-Formals.chpl).
The empty function
proc f((x)) { }accepts a 1-tuple actual with any component type.
When splitting a tuple into multiple formal arguments, the arguments that are grouped using the tuple notation may be omitted. In this case, no names are associated with the omitted components. The call is evaluated as if an argument were defined.
Splitting a Tuple via Tuple Expansion¶
Tuples can be expanded in place using the following syntax:
tuple-expand-expression:
( ... expression )
In this expression, the tuple defined by expression
is expanded in
place to represent its components. This can only be used in a context
where a comma-separated list of components is valid.
Example (expansion.chpl).
Given two 2-tuples
var x1 = (1, 2.0), x2 = ("three", "four");the following statement
var x3 = ((...x1), (...x2));creates the 4-tuple
x3
with the value(1, 2.0, "three", "four")
.
Example (expansion-2.chpl).
The following code defines two functions, a function
first
that returns the initial component of a tuple and a functionrest
that returns a tuple containing all of the remaining components:proc first(t) where isTuple(t) { return t(0); } proc rest(t) where isTuple(t) { proc helper(first, rest...) do return rest; return helper((...t)); }
Value Tuples and Referential Tuples¶
The terms referential tuple and value tuple describe two different ways that tuples capture their components.
A value tuple stores all its components by value and may store components
copy-initialized from another tuple. A value tuple is analogous to
a group of formal arguments that each have the in
intent.
Value tuples include:
tuple variables
formal arguments with
in
intent that have a tuple typetuples returned from functions with the default return intent
A referential tuple stores its components by value or by reference,
as determined by the default argument intent of the component’s type:
if it is const ref
or ref
, the component is a reference,
otherwise the component is stored by value, as though it had the in
intent. A referential tuple is analogous to a group of formal arguments
that each have the default argument intent.
Referential tuples include:
tuple expressions
formal arguments with the default or
const
argument intent that have a tuple type
Rationale
Tuple expressions and other forms of referential tuple are designed to act like a light-weight bundle of arguments. They behave similarly to the individual arguments of a function call.
It would be prohibitively expensive for some argument types (such as arrays) to be copied by default when passed as an argument to a function call.
The same logic applies to tuple expressions. When the default argument intent of a value’s type is some form of
ref
, a tuple expression will capture the value by reference in order to avoid a potentially expensive copy operation.
In short, some or all of the components of a referential tuple may be references, while a value tuple will never contain a reference.
Example (tuple-expression-behavior.chpl).
In the following example:
record R { var x: int; } var a: [0..0] int; var i: int; var r: R; // // The int `i` is copied when captured into the tuple expression, // but `a` and `r` are not. // test((a, i, r)); // Modify the globals, then print the tuple. proc test(tup) { a[0] = 1; i = 2; r.x = 3; // Outputs (1, 0, (x = 3)). writeln(tup); }The tuple expression
(a, i, r)
will capture the arraya
and the recordr
by reference, but will create a copy of the integeri
.Example (tuple-variable-behavior.chpl).
In the following example:
record R { var x: int; } var a: [0..0] int; var i: int; var r = new R(0); // The tuple variable `tup` stores copies of `a`, `i`, and `r`. var tup = (a, i, r); a[0] = 1; i = 2; r.x = 3; // This will output (0, 0, (x = 0)). writeln(tup);Initialization of the tuple variable
tup
will copy-initialize its components from the arraya
, the recordr
, and the integeri
, see Copy and Move Initialization. Becausetup
stores a copy of these three variables, changes made to them are not visible intup
when it is written to standard output.
Tuple Argument Intents¶
A formal argument of a tuple type may be either a referential tuple or a value tuple depending on its argument intent.
If the tuple argument has the default argument intent, then it is a referential tuple and some of its components may be captured by
ref
depending on their default argument intent. If a value tuple (such as a tuple variable) is passed to it, the value tuple will be implicitly converted into a referential tuple. The resulting referential tuple may refer to components from the original value tuple.
A tuple argument declared with
const
intent works similarly to one with a default intent, except that all the components of the tuple are considered to beconst
and cannot be modified.
If the tuple argument has the
in
orconst in
intent, then it is a value tuple. Each of its components is copy-initialized from the corresponding component of the actual argument as if it is passed to anin
intent formal argument. When the actual argument is a referential tuple and the component is a reference, the source of copy initialization is the variable being referenced. All components of aconst in
argument are considered to beconst
and cannot be modified.
If the tuple argument has the
ref
orconst ref
intent, then it is a reference to a tuple from the call site. The actual argument must be a value tuple (such as a tuple variable). The formal argument will be a single reference to to that value tuple. All components of aconst ref
argument are considered to beconst
and cannot be modified.
Example (tuple-argument-behavior.chpl).
record R { var x: int; } var modTup = (0, new R(0)); // // The argument `tup` of `referentialTupleArg` is a referential tuple // due to the default argument intent. // proc referentialTupleArg(tup) { // Modify the module variable `modTup`. modTup = (3, new R(6)); // // Should print (0, (x = 6)). Recall that a tuple argument with the // default argument intent copies integer elements. // writeln(tup); // // When `tup` is passed to `valueTupleArg`, a copy of each element // is made because the `valueTup` argument has the `in` intent. // valueTupleArg(tup); // Should still print (0, (x = 6)). writeln(tup); } // The argument `valueTup` is a value tuple due to the `in` intent. proc valueTupleArg(in valueTup) { valueTup = (64, new R(128)); } // // When `modTup` is passed to `referentialTupleArg`, its first // element is copied while its second element is passed as though // it were `const ref`. // referentialTupleArg(modTup);Example (tuple-argument-ref-intent.chpl).
// // Because the intent of `tup` is `ref`, only value tuples can be // passed to `passTupleByRef`. // proc passTupleByRef(ref tup) { tup = (64, 128); } var modTup = (0, 0); // // Passing `modTup` to `passTupleByRef` will construct a referential // tuple where each element refers to an element from `modTup`. // passTupleByRef(modTup); // Should print (64, 128). writeln(modTup);
Tuple Return Behavior¶
Procedures with the default and out
return intent always return
a value tuple. If an expression returned by such a procedure is
a referential tuple, it will be implicitly converted to a value tuple.
When a tuple is returned from a procedure with ref
or const ref
return intent, it must be a value tuple that exists outside of
the procedure’s scope. Otherwise there is a compilation error.
Example (tuple-return-behavior.chpl).
record R { var x: int; } var a: [0..0] int; var i: int; var r = new R(0); // // The value tuple returned by `returnTuple` is passed to the // function `updateGlobalsAndOutput`. It is implicitly converted // into a referential tuple because the formal argument `tup` // has the default argument intent. // updateGlobalsAndOutput(returnTuple()); // // The function `returnTuple` returns a value tuple that contains // a copy of the array `a`, the integer `i`, and the record `r`. // proc returnTuple() { return (a, i, r); } proc updateGlobalsAndOutput(tup) { a[0] = 1; i = 2; r.x = 3; // // Because the tuple passed to `updateGlobalsAndOutput` is a value // tuple and contains no references, the assignments made to `a`, // `i`, and `r` above are not visible in `tup` when it is printed. // This `writeln` will output (0, 0, (x = 0)). // writeln(tup); }
Tuple Yield Behavior¶
Iterators with the out
yield intent always yield
a value tuple. If an expression yielded by such an iterator is
a referential tuple, it will be implicitly converted to a value tuple.
A tuple yielded from an iterator with ref
or const ref
yield intent must be a value tuple.
Rationale
Tuple yield behavior matches tuple return behavior, except yielding a local tuple is allowed. This is because the iterator will generally continue executing after the yield statement completes.
Open issue.
We also need to provide a mechanism to yield referential tuples.
An iterator with the default or const
yield intent may yield
using the semantics of either the out
or const ref
yield intent,
in an implementation-defined manner.
Tuple Operators¶
Unary Operators¶
The unary operators +
, -
, ~
, and !
are overloaded on
tuples by applying the operator to each argument component and returning
the results as a new tuple.
The size of the result tuple is the same as the size of the argument tuple. The type of each result component is the result type of the operator when applied to the corresponding argument component. For example:
Example (unary-ops.chpl).
The following code:
var x = -(-1, 5, -3.14, 99.9);creates a 3-tuple,
x
, with the value(1, -5, 3.14, -99.9)
which has the same type as the tuple-literal on the right side of the expression.
The type of every element of the operand tuple must have a well-defined operator matching the unary operator being applied. That is, if the element type is a user-defined type, it must supply an overloaded definition for the unary operator being used. Otherwise, a compile-time error will be issued.
Binary Operators¶
The binary operators +
, -
, *
, /
, %
, **
, &
,
|
, ^
, <<
, and >>
are overloaded on tuples by applying
them to pairs of the respective argument components and returning the
results as a new tuple. The sizes of the two argument tuples must be the
same. These operators are also defined for homogeneous tuples and scalar
values of matching type.
The size of the result tuple is the same as the argument tuple(s). The type of each result component is the result type of the operator when applied to the corresponding pair of the argument components.
When a tuple binary operator is used, the same operator must be well-defined for successive pairs of operands in the two tuples. Otherwise, the operation is illegal and a compile-time error will result.
Example (binary-ops.chpl).
The code:
var x = (1, 1, "1") + (2, 2.0, "2");creates a 3-tuple of an int, a real and a string with the value
(3, 3.0, "12")
.
Relational Operators¶
The relational operators >
, >=
, <
, <=
, ==
, and
!=
are defined over tuples of matching size. They return a single
boolean value indicating whether the two arguments satisfy the
corresponding relation.
The operators >
, >=
, <
, and <=
check the corresponding
lexicographical order based on pair-wise comparisons between the
argument tuples’ components. The operators ==
and !=
check
whether the two arguments are pair-wise equal or not. The relational
operators on tuples may be short-circuiting, i.e. they may execute only
the pair-wise comparisons that are necessary to determine the result.
However, just as for other binary tuple operators, the corresponding operation must be well-defined on each successive pair of operand types in the two operand tuples. Otherwise, a compile-time error will result.
Example (relational-ops.chpl).
The code:
var x = (1, 1, 0) > (1, 0, 1);creates a variable initialized to
true
. After comparing the first components and determining they are equal, the next components are compared to determine that the first tuple is greater than the second tuple.
Predefined Routines on Tuples¶
- proc tuple.size param¶
Returns the size of the tuple.
- proc tuple.indices¶
Returns the range
0..<this.size
representing the indices that are legal for indexing into the tuple.
- proc isHomogeneousTuple(t: tuple) param¶
Returns true if
t
is a homogeneous tuple; otherwise false.
- proc isTuple(type t) param¶
Returns true if
t
is a tuple; otherwise false.
- proc isTupleType(type t) param¶
Returns true if
t
is a tuple of types; otherwise false.
- operator :(x: (?, ?), type t: complex(64))¶
Cast from a generic two-tuple to a
complex(64)
- operator :(x: (?, ?), type t: complex(128))
Cast from a generic two-tuple to a
complex(128)
- proc max(type t): t where isTupleType(t)¶
Returns a tuple of type t with each component set to
max
of the type in the corresponding component of the argument.
- proc min(type t): t where isTupleType(t)¶
Returns a tuple of type t with each component set to
min
of the type in the corresponding component of the argument.