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
.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 (each, map, filter, fold)
range(0, 5) -> each { $ * 2 }           # [0, 2, 4, 6, 8]
repeat("x", 3) -> each { $ }            # ["x", "x", "x"]
[1, 2, 3] -> .first() -> each { $ } # 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) -> map { $ * 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

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() -> each { $ * 2 }    # list[2, 4, 6]
"hello" -> .first() -> each { $ }              # ["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() -> !$.done @ {
  $.value -> log
  $.next()
}
# logs: h, e, l, l, o

Preferred: use each for iteration:

"hello" -> each { 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
Operatorseach, map, filter, foldSame four 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 each, map, filter, and fold 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) -> each { $ }    # [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) -> each { $ }    # [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) -> map { $ * $ } -> fold(0) { $@ + $ }
# 385 (1 + 4 + 9 + ... + 100)

Generate index markers

range(0, 5) -> each { "Item {$}" }
# ["Item 0", "Item 1", "Item 2", "Item 3", "Item 4"]

Retry pattern

repeat(1, 3) -> each {
  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) -> each { $ => $row -> range(1, 4) -> each { $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) -> each { $ }    # ERROR: Iterator exceeded 10000 elements

See Also

  • Collectionseach, map, filter, fold operators
  • Closures — Closure semantics for custom iterators
  • Reference — Complete language specification