Iterators
Overview
Iterators provide lazy sequence generation in rill. They produce values on demand rather than materializing entire collections upfront.
Built-in iterators:
| Function | Description |
|---|---|
range(start, end, step?) | Generate number sequence |
repeat(value, count) | Repeat value n times |
iterate(seed, closure) | Infinite stream: apply closure to produce next value |
.first() | Get iterator for any collection |
Key characteristics:
- Value-based:
.next()returns a new iterator, original unchanged - Lazy: Elements generated on demand
- Composable: Work with all collection operators (
seq,fan,filter,fold,acc)
range(0, 5) -> seq({ $ * 2 }) # [0, 2, 4, 6, 8]
repeat("x", 3) -> seq({ $ }) # ["x", "x", "x"]
[1, 2, 3] -> .first() -> seq({ $ }) # list[1, 2, 3]Iterator Protocol
Iterators are dicts with three fields:
| Field | Type | Description |
|---|---|---|
value | any | Current element (absent when done) |
done | bool | True if exhausted |
next | closure | Returns new iterator at next position |
# Iterator structure
[
value: 0,
done: false,
next: || { ... } # returns new iterator
]Collection operators automatically recognize and expand iterators:
range(1, 4) -> fan({ $ * 10 }) # [10, 20, 30]
range(0, 10) -> filter({ $ > 5 }) # [6, 7, 8, 9]
range(1, 6) -> fold(0, { $@ + $ }) # 15Built-in Iterators
range(start, end, step?)
Generate a sequence of numbers from start (inclusive) to end (exclusive).
| Parameter | Type | Default | Description |
|---|---|---|---|
start | number | required | First value |
end | number | required | Stop value (exclusive) |
step | number | 1 | Increment (can be negative) |
range(0, 5) # 0, 1, 2, 3, 4
range(1, 6) # 1, 2, 3, 4, 5
range(0, 10, 2) # 0, 2, 4, 6, 8
range(5, 0, -1) # 5, 4, 3, 2, 1
range(-3, 2) # -3, -2, -1, 0, 1
range(0, 1, 0.25) # 0, 0.25, 0.5, 0.75Edge cases:
range(5, 5) # empty (start == end)
range(5, 3) # empty (start > end with positive step)
range(0, 5, -1) # empty (wrong direction)
range(0, 5, 0) # ERROR: step cannot be zerorepeat(value, count)
Generate a value repeated n times.
| Parameter | Type | Description |
|---|---|---|
value | any | Value to repeat |
count | number | Number of repetitions |
repeat("x", 3) # "x", "x", "x"
repeat(0, 5) # 0, 0, 0, 0, 0
repeat([a: 1], 2) # dict[a: 1], dict[a: 1]
trueEdge cases:
repeat("x", 0) # empty
repeat("x", -1) # ERROR: count cannot be negativeiterate: Infinite Source Stream
Signature: iterate(seed, closure): stream of T
iterate produces an unbounded stream. It starts with seed as the first emitted value. After each emission, it calls closure with the current value in $ to produce the next value, which becomes both the next emission and the next seed.
The stream is always infinite. You must bound it externally with take(n), or accept that the iteration ceiling (RILL_R010) will halt execution.
Pipe form
When piped, the piped value becomes seed automatically:
0 -> iterate({ $ + 1 }) -> take(5)
# Result: [0, 1, 2, 3, 4]Explicit form
iterate(0, { $ + 1 }) -> take(5)
# Result: [0, 1, 2, 3, 4]Bounding the stream
Use take(n) to collect a fixed count:
iterate(1, { $ * 2 }) -> take(6)
# Result: [1, 2, 4, 8, 16, 32]Use stop_when to bound by a content condition. Because iterate produces an infinite stream, you must combine stop_when with take as a safety bound:
# stop_when without take exhausts the iteration ceiling on an infinite stream
# Use take first to cap the search range:
iterate(1, { $ + 1 }) -> take(20) -> stop_when({ $ >= 5 })
# Result: [1, 2, 3, 4, 5]Fibonacci sequence
Use a dict pair as the seed to carry two values between steps. The closure advances the pair; seq extracts the a field from each step.
iterate(dict[a: 0, b: 1], { dict[a: $.b, b: ($.a + $.b)] }) -> take(8) -> seq({ $.a })
# Result: [0, 1, 1, 2, 3, 5, 8, 13]Iteration ceiling
Consuming more than 10,000 chunks without bounding halts execution with RILL_R010. The halt fires at the materializing consumer, such as seq:
# Error: RILL_R010 — iteration ceiling exceeded
iterate(0, { $ + 1 }) -> seq({ $ })take(n) clamps silently to MAX_ITER (10,000) without itself halting. The ceiling fires only when the downstream consumer forces iteration past 10,000 elements.
Error contracts
| Condition | Error |
|---|---|
| Closure is missing or not invocable | Catchable halt: RILL_R006 (EC-17) |
| Closure produces a catchable halt | Propagates as catchable (EC-14) |
| Closure produces a non-catchable halt | Propagates as non-catchable (EC-15) |
| Iteration exceeds 10,000 chunks | Non-catchable halt: RILL_R010 (EC-16) |
# Error: RILL_R006 — closure is required
iterate(0)The .first() Method
Returns an iterator for any collection. Provides a consistent interface for manual iteration.
| Input Type | .first() Returns |
|---|---|
| list | Iterator over elements |
| string | Iterator over characters |
| dict | Iterator over [key: k, value: v] entries |
| iterator | Returns itself (identity) |
[1, 2, 3] -> .first() # iterator at 1
"abc" -> .first() # iterator at "a"
[a: 1, b: 2] -> .first() # iterator at dict[key: "a", value: 1]
range(0, 5) -> .first() # iterator at 0 (identity)
trueEmpty collections return a done iterator:
[] -> .first() # done iterator
"" -> .first() # done iterator
trueUsing .first() with collection operators:
[1, 2, 3] -> .first() -> seq({ $ * 2 }) # list[2, 4, 6]
"hello" -> .first() -> seq({ $ }) # ["h", "e", "l", "l", "o"]Manual Iteration
Traverse an iterator by accessing .value, .done, and calling .next():
[1, 2, 3] -> .first() => $it
# Check if done
$it.done # false
# Get current value
$it.value # 1
# Advance to next position
$it.next() => $it
$it.value # 2Loop pattern (using $ as accumulator):
"hello" -> .first() -> while (!$.done) do {
$.value -> log
$.next()
}
# logs: h, e, l, l, oPreferred: use seq for iteration:
"hello" -> seq({ log($) })
# logs: h, e, l, l, oCheck before access:
[1, 2, 3] => $list
$list -> .first() => $it
$it.done ? "empty" ! $it.valueStreams vs Iterators
Streams and iterators share the same protocol fields but differ in source, statefulness, and invocation.
| Property | Iterator | Stream |
|---|---|---|
| Source | Synchronous | Asynchronous |
| Statefulness | Stateless, re-iterable | Stateful, single-use |
| Protocol | done, value, next | done, value, next (shared) |
| Stale access | N/A (immutable) | Halts with error |
| Operators | seq, fan, filter, fold, acc | Same operators |
| Invocation | Not callable | $s() returns resolution value |
| Production | Built-in functions | Host helper or :stream(T):R closure |
Iterators are value types: calling .next() returns a new iterator and the original is unchanged. Streams are stateful: each chunk consumed advances the stream permanently.
See Types for stream type signatures and chunk semantics. See Collections for how seq, fan, filter, fold, and acc behave on streams.
Custom Iterators
Create custom iterators by implementing the protocol:
# Counter from start to max
|start, max| [
value: $start,
done: ($start > $max),
next: || { $counter($.value + 1, $max) }
] => $counter
$counter(1, 5) -> seq({ $ }) # [1, 2, 3, 4, 5]Fibonacci sequence:
|a, b, max| [
value: $a,
done: ($a > $max),
next: || { $fib($.b, $.a + $.b, $max) }
] => $fib
$fib(0, 1, 50) -> seq({ $ }) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]Infinite iterator (use with limit):
|n| [
value: $n,
done: false,
next: || { $naturals($.value + 1) }
] => $naturals
# Take first 5 using fold with compound accumulator
$naturals(1) -> .first() -> fold([list: [], it: $], {
($@.list -> .len >= 5) ? $@ -> break ! [
list: [...$@.list, $@.it.value],
it: $@.it.next()
]
}) -> $.list # [1, 2, 3, 4, 5]Element Access: .head and .tail
For direct element access (not iteration), use .head and .tail:
| Method | Description |
|---|---|
.head | First element (errors on empty) |
.tail | Last element (errors on empty) |
[1, 2, 3] -> .head # 1
[1, 2, 3] -> .tail # 3
"hello" -> .head # "h"
"hello" -> .tail # "o"Empty collections error (no null in rill):
[] -> .head # ERROR: Cannot get head of empty list
"" -> .tail # ERROR: Cannot get tail of empty stringComparison with .first():
| Method | Returns | On Empty |
|---|---|---|
.head | Element directly | Error |
.first() | Iterator | Done iterator |
Examples
Sum of squares
range(1, 11) -> fan({ $ * $ }) -> fold(0, { $@ + $ })
# 385 (1 + 4 + 9 + ... + 100)Generate index markers
range(0, 5) -> seq({ "Item {$}" })
# ["Item 0", "Item 1", "Item 2", "Item 3", "Item 4"]Retry pattern
repeat(1, 3) -> seq({
attempt() => $result
($result.success == true) ? ($result -> break)
pause("00:00:01")
$result
})Filter even numbers
range(0, 20) -> filter({ ($ % 2) == 0 })
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]Nested iteration
range(1, 4) -> seq({ $ => $row -> range(1, 4) -> seq({ $row * $ }) })
# [list[1, 2, 3], list[2, 4, 6], list[3, 6, 9]]Limits
Iterators are expanded eagerly when passed to collection operators. A default limit of 10000 elements prevents infinite loops:
# This would error after 10000 elements
|n| [value: $n, done: false, next: || { $inf($.value + 1) }] => $inf
$inf(0) -> seq({ $ }) # ERROR: Iterator exceeded 10000 elementsBound an infinite iterator with take(n) or break to stay within the ceiling:
$inf(0) -> take(5) # list[0, 1, 2, 3, 4]
[1, 2, 3] -> cycle -> take(6) # list[1, 2, 3, 1, 2, 3]See Collection Slicing for take, skip, cycle, and other slicing operators.
See Also
- Collections —
seq,fan,filter,fold,acc,sortoperators - Collection Slicing —
take,skip,cycle,batch,window,start_when,stop_when,debounce,throttle,sample - Closures — Closure semantics for custom iterators
- Reference — Complete language specification