Iterators

Overview

Iterators provide lazy sequence generation in rill. They produce values on demand rather than materializing entire collections upfront.

Built-in iterators:

FunctionDescription
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:

FieldTypeDescription
valueanyCurrent element (absent when done)
doneboolTrue if exhausted
nextclosureReturns 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, { $@ + $ }) # 15

Built-in Iterators

range(start, end, step?)

Generate a sequence of numbers from start (inclusive) to end (exclusive).

ParameterTypeDefaultDescription
startnumberrequiredFirst value
endnumberrequiredStop value (exclusive)
stepnumber1Increment (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.75

Edge 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 zero

repeat(value, count)

Generate a value repeated n times.

ParameterTypeDescription
valueanyValue to repeat
countnumberNumber 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]
true

Edge cases:

repeat("x", 0)        # empty
repeat("x", -1)       # ERROR: count cannot be negative

iterate: 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

ConditionError
Closure is missing or not invocableCatchable halt: RILL_R006 (EC-17)
Closure produces a catchable haltPropagates as catchable (EC-14)
Closure produces a non-catchable haltPropagates as non-catchable (EC-15)
Iteration exceeds 10,000 chunksNon-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
listIterator over elements
stringIterator over characters
dictIterator over [key: k, value: v] entries
iteratorReturns 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)
true

Empty collections return a done iterator:

[] -> .first()           # done iterator
"" -> .first()               # done iterator
true

Using .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                    # 2

Loop pattern (using $ as accumulator):

"hello" -> .first() -> while (!$.done) do {
  $.value -> log
  $.next()
}
# logs: h, e, l, l, o

Preferred: use seq for iteration:

"hello" -> seq({ log($) })
# logs: h, e, l, l, o

Check before access:

[1, 2, 3] => $list
$list -> .first() => $it
$it.done ? "empty" ! $it.value

Streams vs Iterators

Streams and iterators share the same protocol fields but differ in source, statefulness, and invocation.

PropertyIteratorStream
SourceSynchronousAsynchronous
StatefulnessStateless, re-iterableStateful, single-use
Protocoldone, value, nextdone, value, next (shared)
Stale accessN/A (immutable)Halts with error
Operatorsseq, fan, filter, fold, accSame operators
InvocationNot callable$s() returns resolution value
ProductionBuilt-in functionsHost 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:

MethodDescription
.headFirst element (errors on empty)
.tailLast 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 string

Comparison with .first():

MethodReturnsOn Empty
.headElement directlyError
.first()IteratorDone 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 elements

Bound 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

  • Collectionsseq, fan, filter, fold, acc, sort operators
  • Collection Slicingtake, skip, cycle, batch, window, start_when, stop_when, debounce, throttle, sample
  • Closures — Closure semantics for custom iterators
  • Reference — Complete language specification