Specifications / Language

Iterators and Iteration

Default and ML use range pipelines rather than for, while or loop. Python flavor also accepts for name in iterator: and list comprehensions, lowering them to the same iterator operations. It rejects while, loop else, break and continue.

Core Iterator Functions — [BUILTIN-ITER]

Iterator<T> is the opaque type produced by range. It is distinct from a materialized List<T> and is consumed by forEach, fold or toList.

range(start: int, end: int) -> Iterator<int> — [BUILTIN-ITER-RANGE]

Generates integers from start (inclusive) to end (exclusive).

range(1, 5)      // 1, 2, 3, 4
range (1, 5)     // 1, 2, 3, 4

forEach(iterator: Iterator<T>, function: fn(T) -> Unit) -> Unit — [BUILTIN-ITER-FOREACH]

Applies function to each element for its side effects.

range(1, 5) |> forEach(print)
range (1, 5) |> forEach print

map(iterator: Iterator<T>, function: fn(T) -> U) -> Iterator<U> — [BUILTIN-ITER-MAP]

Transforms each element.

range(1, 5) |> map(double)
range (1, 5) |> map double

filter(iterator: Iterator<T>, predicate: fn(T) -> bool) -> Iterator<T> — [BUILTIN-ITER-FILTER]

Keeps elements that satisfy predicate.

range(1, 10) |> filter(isEven)
range (1, 10) |> filter isEven

fold(iterator: Iterator<T>, initial: U, function: fn(U, T) -> U) -> U — [BUILTIN-ITER-FOLD]

Reduces an iterator to a single value. A list-literal seed (fold([], …), fold([1, 2], …)) takes the runtime list representation exactly once, before the loop, so every iteration's listAppend or listConcat reads and returns a runtime list and the loop body performs no per-iteration conversion.

fn add(total: int, value: int) -> int = total + value
range(1, 5) |> fold(0, add)   // 0+1+2+3+4 = 10
add : (int, int) -> int
add (total, value) = total + value

range (1, 5) |> fold (0, add)   // 0+1+2+3+4 = 10

toList(iterator: Iterator<T>) -> List<T> — [BUILTIN-ITER-TOLIST]

Runs the pipeline once and collects every element it yields, in order, into a List<T>. It is the bridge from a pipeline back to a materialized list.

range(1, 7) |> filter(isEven) |> toList   // [2, 4, 6]
range (1, 7) |> filter isEven |> toList   // [2, 4, 6]

Callbacks and Accumulators — [BUILTIN-ITER-CALLBACK]

Callbacks may be lambdas, named functions, or function values. Generic named functions are specialized at the call site. Iterator combinators preserve a callback's complete return type, including Result<T, E>; they never unwrap a failure channel. Arithmetic in a callback needs no fallback, because it produces no Result; an arithmetic fault inside a lambda passed to a combinator propagates to the enclosing Arith handler, or the program is rejected (ARITH-TOTAL).

fn energy(p) = p.mass + p.spin      // generic: inferred, no annotations
fn addEnergy(total: int, value: int) -> int = total + value
range(1, n) |> map(forge) |> map(energy) |> fold(0, addEnergy)

A fold accumulator may be any inferred type, including a record:

fn bump(p, step) = p { mass: p.mass + 1 }
range(1, n) |> fold(Particle { id: 0, mass: 0, spin: 0 }, bump)   // -> Particle

Pipe Operator — [BUILTIN-ITER-PIPE]

|> passes its left operand as the first argument to the function on its right.

5 |> double |> print                                        // print(double(5))
range(1, 10) |> forEach(print)
range(0, 20) |> filter(isEven) |> map(double) |> forEach(print)
5 |> double |> print                                        // print(double(5))
range (1, 10) |> forEach print
range (0, 20) |> filter isEven |> map double |> forEach print

Stream Fusion — [BUILTIN-ITER-FUSION]

Chains of map and filter over a range are emitted as one loop when consumed by forEach, fold or toList; no intermediate collection is created:

range(1, 5) |> map(double) |> filter(isEven) |> forEach(print)
range (1, 5) |> map double |> filter isEven |> forEach print

is equivalent to:

for (i = 1; i < 5; i++) {
    value = double(i);
    if (isEven(value)) print(value);
}

A pipeline's stages belong to the iterator they were applied to. A pipeline bound to a name keeps them, two pipelines built on one source are independent, and consuming a pipeline twice runs its stages twice. A lambda stage reads the bindings it closed over where it was written, even when a later statement consumes it.

Fusion happens while the enclosing function is compiled. A pipeline carrying map or filter stages therefore cannot be returned from that function or passed to a function compiled on its own; both are rejected with a `map`/`filter` pipeline leaving the function that built it. Collect it with toList to hand the elements on.