Zippered Iteration

Chapel’s for-loops (and their parallel counterparts, coforall and forall) can be driven by multiple iterands in a coordinated manner known as zippered iteration. This is expressed using the zip keyword, followed by a parenthesized list of the iterands.

As a simple example, the following loop iterates over an array and a range simultaneously, referring to the values yielded by each using distinct index variables, a and i:

var A: [1..5] real;
for (a, i) in zip(A, 1..5) do
  a = i;

As the name suggests, zippered iteration causes the loop body to be executed once per corresponding set of values yielded by the iterands. Thus, in the first iteration of the loop above, a will refer to the first element of A and i to 1; in the second, a refers to A’s second element and i to 2; etc. For this reason, zippered iterands must typically have the same size (i.e., yield the same number of things), though we’ll discuss exceptional cases below.

As a result of this behavior, printing the array after this loop has executed will result in:

1.0 2.0 3.0 4.0 5.0

Naturally, zippering may involve more than two iterands:

for (a, i, j) in zip(A, 1..5, [3, 0, 1, 2, 4]) do
  a = i**j;

Here, the array ends up as:

1.0 1.0 3.0 16.0 625.0

Index Variables for Zippered Iteration

In the example above, we captured the results of the zippering using an index variable per iterand. Zippered iterations can also be written using a single index variable which will store a tuple of the iterands’ values:

for tpl in zip(1..5, [3, 0, 1, 2, 4]) do

The output of this loop is:

(1, 3)
(2, 0)
(3, 1)
(4, 2)
(5, 4)

In fact, the original case can be thought of as de-tupling the zippered index variable in-place, as is permitted in other declaration contexts. Taking this notion further, the following loops show three ways of decomposing the index variable for a zippered iteration over a range and an array of 2-tuples:

for ind in zip(1..3, [(0,2), (3,7), (5,9)]) do
  writeln("ind is ", ind);

for (i,j) in zip(1..3, [(0,2), (3,7), (5,9)]) do
  writeln("i is ", i, ", j is ", j);

for (i, (j,k)) in zip(1..3, [(0,2), (3,7), (5,9)]) do
  writeln("i is ", i, ", j is ", j, ", k is ", k);

The output of these loops is as follows:

ind is (1, (0, 2))
ind is (2, (3, 7))
ind is (3, (5, 9))

i is 1, j is (0, 2)
i is 2, j is (3, 7)
i is 3, j is (5, 9)

i is 1, j is 0, k is 2
i is 2, j is 3, k is 7
i is 3, j is 5, k is 9

As in other de-tupling contexts, this also means that if an iterand’s results are not needed, an underscore can be used to drop its values on the floor:

for (_, i) in zip(A, 1..) do
  writeln("A has at least ", i, " element(s)!");

As with non-zippered for-loops, if none of the iterands’ results are needed, the entire in clause that declares the index variables can be omitted:

writeln("This should iterate five times:");
for zip(A, 1..5) do

Zippered Iterand Size Matching

As mentioned above, zippered iterands must typically have the same size, which is to say they should yield the same number of things. One important exception to this rule, seen above, is that unbounded ranges can be zipped with fixed-size iterands in spite of being conceptually infinite. In such cases, the unbounded range will yield the same number of elements as the bounded iterand(s) with which it is zipped.

As an example, the following loop assigns an array’s elements their ordinal values without needing to know the array’s size a priori:

for (a, i) in zip(A, 1..) do
  a = i;

Unfortunately, there is currently no way for users to define an iterator that is flexible with respect to size in a zippered context like unbounded ranges are. Such a capability is planned for the future to support idioms like iterators that yield an appropriate number of values from an unbounded random sequence or data stream. In the meantime, a typical workaround is to pass such iterators a size argument indicating the number of items to generate.

Note that today, unbounded ranges can occur anywhere in a zippered iterand list. However, in anticipating future support for zippering unbounded user iterators, programmers are encouraged to have any unbounded ranges follow a fixed-size iterand in zippered contexts (as 1.. follows A in the examples above).