Iteration
x2c has two ways to walk a collection. foreach(T name, collection) is the
short form and covers most cases. Iter chains combine operations that process
one element at a time, then collect, aggregate, or loop over the result.
Neither one allocates iterator state on the heap. For an immediately consumed chain, x2c supplies stack storage for every stage. Name that storage yourself when the iterator has to stay live between expressions. The storage convention covers both forms.
The language reference states the foreach rules,
and the standard library overview links to every
iterator operation.
Foreach
The loop header declares the loop variable and names the collection:
List values = %(10 20 30);
int total = 0;
foreach(int value, values)
total += value;
printf("total = %d\n", total);
break and continue behave as they do in a C loop.
What you can iterate
List, Array, Map, and String each provide an iterator adapter in their
own module, so all four work directly:
Array scores = %[3, 4, 5];
foreach(int score, scores)
printf("score = %d\n", score);
Map ages = %{"ada": 36, "alan": 41};
foreach(Var (name, age), ages)
printf("%s is %d\n", name, age.int());
String word = "hi";
foreach(int ch, word)
printf("char = %c\n", ch);
foreach(char ch, word)
printf("char = %c\n", ch);
A Map yields its values, like every other collection. Two binders ask for
the key as well, and the loop reads both out of the table without
allocating. When you need an iterator there are three: map.iter(&storage)
yields the values, map.keys(&storage) yields the keys, and
map.enumerate(&storage) yields (key value) Lists and is the only one
that allocates. The generated families in lib/typed-map.x publish the same
three, so a MapIntInt iterates as a Map does. Map traversal follows
internal slot order, so do not depend on the sequence, and do not insert or
delete entries while a traversal is live. Structural mutation invalidates
outstanding traversal state.
A String yields its bytes as numeric Var values. Declare the loop
variable as int when the byte is used numerically, or as char when it is
passed to a character-oriented C API. Both forms traverse bytes, and neither
decodes Unicode characters.
Lazy String fields
String provides three ways to iterate over fields without first building a
List:
String text = "one two\nthree:four";
foreach(String word, text.words())
printf("word=%s\n", word);
foreach(String line, text.lines())
printf("line=%s\n", line);
foreach(String field, text.splits(":"))
printf("field=%s\n", field);
words() coalesces runs of C whitespace, ignores leading and trailing
whitespace, and yields no empty String. splits(sep) treats every explicit
separator independently, so %" a b ".splits(" ") yields empty fields
around and between the words. lines() recognizes LF, CR, and CRLF and
strips the endings.
These cursors allocate one scope-owned traversal descriptor. There is no
List and no object per field. Each yielded field is a canonical String.
After a field has been interned once, finding an equal field of at most 256
bytes needs no heap allocation. Distinct fields remain in the active String
pool. For a large input, use String.pool_retain() and
String.pool_release() to reclaim temporary fields, and promote the values
that must survive.
A Var is iterated through its descriptor, so a boxed List or Array
iterates the way the unboxed value does. A boxed value with no iterator
adapter produces an already-exhausted iterator, and the loop body runs zero
times. A receiver with a statically known type must have a visible Iter
conformance, from its own adoption or from the nearest typedef ancestor.
typedef List Ast; lets foreach over Ast reuse List’s conformance
without protocol Iter(Ast);. A type with neither is rejected as not
iterable.
An Iter is itself iterable, so foreach can consume a hand-built pipeline.
That combination appears at the end of this chapter.
What the loop variable’s type means
The general Iter path yields a Var. The declared loop type names the
conversion applied to each element as it arrives. It does not filter, and it
does not claim anything about what the collection holds.
foreach(int value, values) means “unbox each element with Var.int” on
that path. Cursor-backed collections deliver their typed outputs directly.
The conversions available for a loop variable are the Var conversions:
char, short, int, uint, long, ulong, float, double,
String, Symbol, List, Array, and Map. Declaring the loop variable
as Var applies no conversion. Do that when the element type varies or when
the body dispatches on the tag:
List values = %(1 2 3);
foreach(Var item, values)
printf("%s\n", item);
If you declare a loop type that has no Var conversion, such as one of your
own structs, x2c still translates the loop, and the C compiler reports an
incompatible assignment to the loop variable. When that happens, pick a
converting type or take a Var and convert in the body.
What foreach guarantees
The collection expression’s static type selects the traversal.
Cursor-backed collections use their typed try_next member. A Var
dispatches through its runtime descriptor, and an Iter is used as it
stands. Foreach stores the traversal state for you and converts each output
to the declared loop type. There is no heap allocation and nothing to clean
up.
Iterators by hand
An Iter points to a small struct holding the object being iterated, one Var
of state, a Func slot, and a next callback that reports success separately
from its result. Advancing it calls next, which fills a Var *out and
returns 1, or returns 0 to report exhaustion. The callback is cleared at
exhaustion.
You read an iterator two ways:
iter.try_next(&out)is the preferred form. It returns success separately from the payload, and writesoutonly when it returns nonzero.iter.next()is a compatibility adapter that returns the element directly andvoidat exhaustion. It is unambiguous because no iterator may yieldvoidas an element.
An Iter is single-pass. There is no rewind, and the aggregates below
consume their input, so build a fresh iterator for a second traversal.
range is the simplest source:
struct Iter storage;
Iter counts = range(1, 4, 1, &storage);
Var value;
while (counts.try_next(&value))
printf("%d\n", value.int());
That prints 1 through 4. A range is inclusive when the endpoint lies in the
step direction. range(3, 1, -1, ...) counts down 3, 2, 1. A step that
would cross the endpoint stops before it, so range(0, 9, 2, ...) yields
0, 2, 4, 6, 8. A range pointed the wrong way is empty, and a zero step is
rejected at construction.
You can write a source yourself by initializing storage with your own
callback. Iter.init takes the object, the callback, and the initial state,
and the receiver is the storage:
static int _powers_next(Iter iter, Var *out) {
long value = iter.state.long();
if (value > iter.obj.long()) return 0;
*out = (int) value;
iter.state = value * 2;
return 1;
}
int main(void) {
struct Iter storage;
Iter powers = Iter.init(&storage, 64, _powers_next, 1);
foreach(int value, powers)
printf("%d\n", value);
return 0;
}
Inside such a callback, write iter->next when you need the field.
iter.next(...) is receiver syntax and calls the method instead of the
stored callback.
The storage convention
An Iter is a pointer to caller-owned storage. Each source and lazy
operation takes a final Iter destination internally, initializes it, and
returns the same pointer. The operation’s state is stored in that
struct Iter. There is no second state object and nothing to free.
Fluent code omits those destinations when the whole chain is consumed immediately:
static Var double_value(Var value) {
return value * 2;
}
static int divisible_by_four(Var value) {
return value % 4 == 0;
}
int main(void) {
Scope.retain();
Array values = range(1, 8, 1)
.map(double_value)
.filter(divisible_by_four)
.array();
for (size_t i = 0; i < values.len(); i++)
printf("%d\n", values[i].int());
Scope.release();
return 0;
}
x2c inserts a distinct struct Iter compound literal for each missing final
destination. Those objects have automatic storage and remain alive through
the enclosing block. Completion applies only when the chain ends in
try_next, next, done, list, array, reduce, foldl, any, all,
find, count, sum, product, min, max, or foreach.
It does not apply when an iterator is assigned, returned, or passed as an
argument. Then declare one struct Iter for each stage:
static Var double_value(Var value) { return value * 2; }
static int divisible_by_four(Var value) { return value % 4 == 0; }
int main(void) {
struct Iter source_storage, map_storage, filter_storage;
Iter source = range(1, 8, 1, &source_storage);
Iter doubled = source.map(double_value, &map_storage);
Iter selected = doubled.filter(divisible_by_four, &filter_storage);
Var first = selected.next();
// selected and all of its sources remain available here.
return first is void;
}
Storage must outlive every iterator that refers to it. Never return an Iter
built over local storage; take the destination storage as a parameter or
return a collected List or Array. One struct Iter holds one live stage,
so reusing it within a pipeline overwrites the earlier stage.
A lazy callback stage also stores its Func. A direct function uses one
file-static binding, and a dynamic function pointer or capturing lambda
belongs to the active Scope. That Scope must stay alive until the iterator
is finished. To return a lazy iterator, its source storage, destination
storage, and any dynamic or captured callback must outlive the returned
value.
The iterators need no cleanup. A collected List or Array is scope-owned
data, so the first program brackets its work with Scope.retain and
Scope.release. See scopes and lifetime for what that pair
does.
Pipeline operations
These operations are lazy. The signatures show the fluent form; add a final
&storage when retaining a result. There is no take_while, flat_map, or
lazy sort. For eager transformations over a List, see
collections.
The callback operations accept Func, so direct functions, function-pointer
values, and capturing lambdas use the same method names. Every source
element and accumulator is passed as a value. A callback with a reference
parameter is rejected when it is first invoked, and it never receives an
alias into source storage. An empty source does not invoke or arity-check
its callback.
range(start, end, step): eachintin the range.iter.filter(pred): elements accepted byVartruthiness.iter.map(fn):fn(element).iter.head(count): at mostcountleading elements.iter.enumerate(start):(index element)Lists.iter.unique(): the first occurrence of each element.iter.chain(other): all ofiter, then all ofother.iter.zip(other):(left right)Lists until either side ends.iter.zip_with(other, fn):fn(left, right)pairwise.iter.map2(other, fn): the same pairwise mapping withfnrequired.iter.scan(seed, fn): each new accumulator.iter.accumulate(initial): a running numeric sum withVarpromotion.iter.unzip(&shared, &storage): two independent column iterators. This operation always keeps both arguments explicit.Iter.repeat(value, count):value,counttimes.
Some operations need a little explanation:
filterkeeps its predicate in the destinationIter, like the callbacks used by the other lazy operations.scanapplies its callback to the accumulator and each element and yields every new accumulator, never the seed. Its callback must not returnvoid.accumulateis the numeric case. It adds throughVar.binary, keeps the promoted result tag, and yields each running total. Usescanfor anything else.map2iszip_withwith the callback required. Azip_withgiven no callback yields pairs.unziprequires two-elementLists and rejects other shapes. It yields two column iterators that can be consumed independently, buffering only the lag between them.headandrepeattreat a negative count as zero.
Nothing runs until you ask for an element. Each stage then requests an
element from its source. Taking two elements from a map over a range calls
the mapping function twice. The pipeline above starts when array() requests
its first element.
Collecting and folding
Constructing an iterator does not consume it. Collectors and aggregates do:
static int over_two(Var value) { return value > 2; }
int main(void) {
List numbers = range(1, 4, 1).list();
Array boxed = range(1, 4, 1).array();
printf("%s and %s\n", numbers.str(), boxed.str());
printf("sum = %d\n", range(1, 10, 1).sum().int());
printf("first over two = %s\n",
range(1, 10, 1).find(over_two));
return 0;
}
iter.list() and iter.array() build a fresh List or Array under the
current scope. The aggregates are count, sum, product, min, max,
reduce, foldl, any, all, and find. sum and product use Var
arithmetic promotion. min and max use total Var ordering and keep the
first of equal values. min, max, and find return void when there is
nothing to return, and reduce given a void initial value uses the first
element as its seed.
Handing a pipeline to foreach
An Iter is iterable, so foreach can consume a fluent pipeline directly.
The compiler supplies stack storage for every stage, including both sides of
a zip or map2, before the loop starts pulling.
static int is_even(Var value) { return value % 2 == 0; }
int main(void) {
foreach(int value, range(1, 10, 1).filter(is_even))
printf("%d\n", value);
return 0;
}
Nested foreach loops receive separate storage for each chain. Use the
explicit form when the same iterator must be paused and resumed outside one
loop.
Choosing between the two
Use foreach when the collection has an adapter, the element conversion you
want exists, and the loop body is the interesting part. It is shorter and it
cannot leak an iterator.
Drive an Iter by hand when:
- you are composing streaming stages, especially over a
range; - you need to interleave or compare two sources with
zip,zip_with, orchain; - exhaustion status matters to the surrounding code, so you want
try_nextinstead of a loop that ends silently; - you are writing a new source with
Iter.init; - the traversal has to be paused, handed to another function, or resumed. The loop form cannot express that.
Idioms shows how iteration combines with other x2c features.