Collection Operators
Overview
rill provides five collection operators for transforming, filtering, and reducing data:
| Operator | Execution | Accumulator | Returns | Catches break? |
|---|---|---|---|---|
seq | Sequential | No | List of all results | Yes |
fan | Parallel | No | List of all results | No |
filter | Parallel | No | Elements where predicate is true | No |
fold | Sequential | Required | Final result only | No |
acc | Sequential | Required | List of all intermediate results | Yes |
All five operators are host-callable functions. They share similar invocation patterns but differ in execution model and output.
Important: Loop bodies cannot modify outer-scope variables (see Variables). Use
foldoraccwith accumulators instead.
# Sequential: results in order, one at a time
[1, 2, 3] -> seq({ $ * 2 })
# Result: [2, 4, 6]
# Parallel: results in order, concurrent execution
[1, 2, 3] -> fan({ $ * 2 })
# Result: [2, 4, 6]
# Parallel filter: keep matching elements
[1, 2, 3, 4, 5] -> filter({ $ > 2 })
# Result: [3, 4, 5]
# Reduction: accumulates to single value
[1, 2, 3] -> fold(0, { $@ + $ })
# Result: 6
# Scan: accumulates, returns all intermediate values
[1, 2, 3] -> acc(0, { $@ + $ })
# Result: [1, 3, 6]seq — Sequential Iteration
seq iterates over a collection in order. Each iteration completes before the next begins. Returns a list of all body results.
# Double each number
[1, 2, 3] -> seq({ $ * 2 })
# Result: [2, 4, 6]
# Transform strings
["a", "b", "c"] -> seq({ "{$}!" })
# Result: ["a!", "b!", "c!"]
# Iterate string characters
"hello" -> seq({ $ })
# Result: ["h", "e", "l", "l", "o"]Dict Iteration
When iterating over a dict, $ contains key and value fields.
[name: "alice", age: 30] -> seq({ "{$.key}: {$.value}" })
# Result: ["name: alice", "age: 30"]
[a: 1, b: 2, c: 3] -> seq({ $.value * 2 })
# Result: [2, 4, 6]Variable Closure
Pass a pre-defined closure by reference.
|x| ($x * 2) => $double
[1, 2, 3] -> seq($double)
# Result: [2, 4, 6]Early Termination
seq catches break. Returns partial results collected before the break.
[1, 2, 3, 4, 5] -> seq({
($ == 3) ? break
$ * 2
})
# Result: [2, 4]Empty Collections
seq returns [] for empty collections. The body never executes.
[] -> seq({ $ * 2 })
# Result: []Streams with seq
use<ext:app> => $app
# Stream: each chunk is one call
$app.lines("file.txt") -> seq({ $ -> .upper })
# Returns list of uppercased linesAn empty stream returns [] without executing the body.
fan — Parallel Iteration
fan iterates concurrently. Order is preserved despite parallel execution. Does not catch break.
# Map with closure parameter
["a", "b", "c"] -> fan({ "{$}!" })
# Result: ["a!", "b!", "c!"]
# Block expression (implicit $)
[1, 2, 3] -> fan({ $ * 2 })
# Result: [2, 4, 6]Concurrency Limit
Pass an options dict as the second argument to cap concurrent operations.
[1, 2, 3, 4, 5] -> fan({ $ * $ }, [concurrency: 2])
# Result: [1, 4, 9, 16, 25]Key Differences from seq
- No accumulator: Parallel execution has no “previous” value
- No break:
breakin afanbody bubbles up as a halt signal - Concurrent execution: All iterations start immediately
When to Use fan
Use fan when:
- Operations are independent (no shared state)
- Order of execution does not matter (results still ordered)
- I/O-bound operations benefit from concurrency
# CPU-bound: same result as seq, but runs in parallel
[1, 2, 3, 4, 5] -> fan({ $ * $ })
# Result: [1, 4, 9, 16, 25]Empty Collections
fan returns [] for empty collections. The body never executes.
[] -> fan({ $ * 2 })
# Result: []Streams with fan
use<ext:app> => $app
# Each stream chunk is transformed; result is a list
$app.stream_numbers() -> fan({ $ * 2 })
# Returns list[...] — not a streamAn empty stream returns [].
filter — Parallel Filtering
filter keeps elements where the predicate returns true. Predicates must return boolean values. Executes concurrently. Does not catch break.
# Keep numbers greater than 2
[1, 2, 3, 4, 5] -> filter({ $ > 2 })
# Result: [3, 4, 5]
# Keep non-empty strings
["hello", "", "world", ""] -> filter({ !.empty })
# Result: ["hello", "world"]
# Keep even numbers
[1, 2, 3, 4, 5, 6] -> filter({ ($ % 2) == 0 })
# Result: [2, 4, 6]Variable Closure
Pass a pre-defined closure by reference.
|x| ($x > 2) => $gtTwo
[1, 2, 3, 4, 5] -> filter($gtTwo)
# Result: [3, 4, 5]Dict Filtering
When filtering a dict, $ contains key and value fields. Returns list of matching entries.
[a: 1, b: 5, c: 3] -> filter({ $.value > 2 })
# Result: [[key: "b", value: 5], [key: "c", value: 3]]String Filtering
Filters characters in a string.
"hello" -> filter({ $ != "l" })
# Result: ["h", "e", "o"]Chaining with Other Operators
# Filter then transform
[1, 2, 3, 4, 5] -> filter({ $ > 2 }) -> fan({ $ * 2 })
# Result: [6, 8, 10]
# Transform then filter
[1, 2, 3, 4, 5] -> fan({ $ * 2 }) -> filter({ $ > 5 })
# Result: [6, 8, 10]
# Filter, transform, reduce
[1, 2, 3, 4, 5] -> filter({ $ > 2 }) -> fan({ $ * 2 }) -> fold(0, { $@ + $ })
# Result: 24Empty Collections
filter returns [] for empty collections or when nothing matches.
[] -> filter({ $ > 0 })
# Result: []
[1, 2, 3] -> filter({ $ > 10 })
# Result: []Streams with filter
use<ext:app> => $app
# Each stream chunk is tested; result is a list
$app.stream_numbers() -> filter({ $ > 0 })
# Returns list[...] of matching chunks — not a streamAn empty stream returns [].
fold — Sequential Reduction
fold reduces a collection to a single value. Requires a seed and a body callable. Does not catch break.
# Sum numbers
[1, 2, 3] -> fold(0, { $@ + $ })
# Result: 6
# Same with inline closure
[1, 2, 3] -> fold(0, |item|($@ + $item))
# Result: 6Common Patterns
Sum
[1, 2, 3, 4, 5] -> fold(0, { $@ + $ })
# Result: 15Product
[1, 2, 3, 4] -> fold(1, { $@ * $ })
# Result: 24Maximum
[3, 1, 4, 1, 5, 9] -> fold(0, {
($@ > $) ? $@ ! $
})
# Result: 9Count
[1, 2, 3, 4, 5] -> fold(0, { $@ + 1 })
# Result: 5String Join
["a", "b", "c"] -> fold("", { "{$@}{$}" })
# Result: "abc"Typed Accumulator Closure
Use a two-type anonymous closure to declare element and accumulator types.
[1, 2, 3] -> fold(0, |number, number|{ $@ + $ })
# Result: 6Dict Reduction
When folding over a dict, $ contains key and value fields.
[a: 1, b: 2, c: 3] -> fold(0, { $@ + $.value })
# Result: 6Reusable Reducers
Define closures for common reductions.
|x| ($@ + $x) => $summer
|x| (($x > $@) ? $x ! $@) => $maxer
[1, 2, 3] -> fold(0, $summer) => $r1
[3, 7, 2] -> fold(0, $maxer) => $r2
[9, 1, 5] -> fold(0, $maxer) => $r3
$r1
# Result: 6Empty Collections
fold returns the initial value for empty collections. The body never executes.
[] -> fold(0, { $@ + $ })
# Result: 0
[] -> fold(42, { $@ + $ })
# Result: 42Streams with fold
use<ext:app> => $app
# Reduce all stream chunks to a single value
$app.stream_numbers() -> fold(0, { $@ + $ })
# Returns the sum of all chunksAn empty stream returns the initial value without executing the body.
acc — Sequential Scan
acc iterates sequentially with a running accumulator. Unlike fold, it returns a list of all intermediate accumulator values, not just the final one. Catches break.
# Running sum (scan pattern)
[1, 2, 3] -> acc(0, { $@ + $ })
# Result: [1, 3, 6]
# String concatenation scan
["a", "b", "c"] -> acc("", { "{$@}{$}" })
# Result: ["a", "ab", "abc"]Typed Accumulator Closure
Use a two-type anonymous closure to declare element and accumulator types.
[1, 2, 3] -> acc(0, |number, number|{ $@ + $ })
# Result: [1, 3, 6]Early Termination
acc catches break. Returns partial intermediate results collected before the break.
[1, 2, 3, 4, 5] -> acc(0, {
($ == 3) ? break
$@ + $
})
# Result: [1, 3]Empty Collections
acc returns [] for empty collections. The body never executes.
[] -> acc(0, { $@ + $ })
# Result: []Streams with acc
use<ext:app> => $app
# Accumulator persists across stream chunks
$app.stream_numbers() -> acc(0, { $@ + $ })
# Returns running totals across all chunksAn empty stream returns [].
Comparison: acc vs fold
Both acc and fold support accumulators. The difference is what they return.
| Feature | acc | fold |
|---|---|---|
| Returns | List of ALL intermediate results | Final result ONLY |
| Catches break | Yes | No |
| Use case | Scan / prefix-sum | Reduce / aggregate |
Side-by-Side Example
# acc: returns every intermediate result
[1, 2, 3] -> acc(0, { $@ + $ })
# Result: [1, 3, 6]# fold: returns only the final result
[1, 2, 3] -> fold(0, { $@ + $ })
# Result: 6When to Choose
Use acc when you need intermediate states (scan pattern):
# Running balance
[100, -50, 200, -75] -> acc(0, { $@ + $ })
# Result: [100, 50, 250, 175]Use fold when you only need the final result:
# Final balance
[100, -50, 200, -75] -> fold(0, { $@ + $ })
# Result: 175Stream Iteration
Streams produce chunks over time. Collection operators consume all chunks before returning. All stream examples use text fences because stream host functions are unavailable in the test harness.
break in Stream Operators
break stops iteration immediately. The host cleanup function (dispose) runs to release stream resources.
use<ext:app> => $app
# Stop after the first matching chunk; host disposes the stream
$app.log_stream() -> seq({
($ -> .contains("ERROR")) ? break
$
})Stream Chunk Limits
The ^(limit: N) annotation is not valid inside collection operator calls. The parser rejects it with RILL-R081.
use<ext:app> => $app
# Rejected — RILL-R081: ^(limit: N) is not accepted inside seq()
$app.events() -> seq(^(limit: 100) { $ })To stop early, use break inside the operator body. Inner scopes cannot reassign outer variables, so use the incoming value $ or an acc/fold accumulator for stateful conditions:
use<ext:app> => $app
$app.events() -> seq({
($ > 100) ? break
$
})Iteration Ceiling
Exactly 10,000 chunks complete without error. The 10,001st chunk triggers RILL-R010 and halts execution. Use break inside the body to consume at most N chunks and stay within bounds.
use<ext:app> => $app
# Error: exceeds iteration ceiling
$app.infinite_stream() -> seq({ $ })
# RILL-R010: halts on the 10,001st chunkRe-iteration Halt
A consumed stream cannot be re-iterated. Passing a consumed stream to a second operator halts execution with an error.
use<ext:app> => $app
# Error: stream already consumed
$app.stream_numbers() => $s
$s -> seq({ $ })
$s -> seq({ $ }) # Halts: stream is consumedChaining Operators
Combine operators for multi-stage transformations.
# Double each element, then sum
[1, 2, 3] -> fan({ $ * 2 }) -> fold(0, { $@ + $ })
# Result: 12
# Filter even numbers (using parallel filter)
[1, 2, 3, 4, 5] -> filter({ ($ % 2) == 0 })
# Result: [2, 4]
# Complex pipeline: filter, then transform
[1, 2, 3, 4, 5] -> filter({ $ > 2 }) -> fan({ $ * 10 })
# Result: [30, 40, 50]Error Cases
| Case | Example | Error |
|---|---|---|
| Non-callable body | [1,2] -> seq(42) | RILL-R040 |
| Non-iterable input | 42 -> seq({ $ }) | RILL-R002 |
| Vector input | $vec -> seq({ $ }) | RILL-R003 |
| Iteration exceeds 10,000 | range(0, 10001) -> seq({ $ }) | RILL-R010 |
| Options not a dict | [1] -> filter({ $ > 0 }, 42) | RILL-R001 |
| Concurrency not positive integer | [1] -> fan({ $ }, [concurrency: -1]) | RILL-R001 |
| Typed closure type violation | [1,2,3] -> fold(0, |string, number|{ $@ + $ }) | RILL-R001 |
Iterating Different Types
Lists
[1, 2, 3] -> seq({ $ * 2 })
# Result: [2, 4, 6]Strings
Iterates over characters.
"abc" -> seq({ "{$}!" })
# Result: ["a!", "b!", "c!"]Dicts
Iterates over entries with key and value fields.
[a: 1, b: 2] -> seq({ "{$.key}={$.value}" })
# Result: ["a=1", "b=2"]Nested Collections
Process nested structures with nested operators.
# Double nested values
[list[1, 2], list[3, 4]] -> fan({ $ -> seq({ $ * 2 }) })
# Result: [[2, 4], [6, 8]]# Sum all nested values
[list[1, 2], list[3, 4]] -> fold(0, { $@ + ($ -> fold(0, { $@ + $ })) })
# Result: 10Performance Considerations
Sequential vs Parallel
| Scenario | Recommendation |
|---|---|
| CPU-bound computation | seq or fan (similar performance) |
| I/O-bound operations | fan (concurrent benefits) |
| Order-dependent logic | seq (guaranteed order) |
| Stateful accumulation | acc or fold (no parallel option) |
Memory
seq,fan,accallocate result lists proportional to input sizefoldmaintains constant memory (accumulator only)
Quick Reference
# seq - sequential, all results
[1, 2, 3] -> seq({ $ * 2 }) # [2, 4, 6]
# fan - parallel, all results
[1, 2, 3] -> fan({ $ * 2 }) # [2, 4, 6]
[1, 2, 3] -> fan({ $ * 2 }, [concurrency: 2]) # concurrency cap
# filter - parallel, matching elements
[1, 2, 3, 4, 5] -> filter({ $ > 2 }) # [3, 4, 5]
# fold - sequential, final result only
[1, 2, 3] -> fold(0, { $@ + $ }) # 6
# acc - sequential, all intermediate results
[1, 2, 3] -> acc(0, { $@ + $ }) # [1, 3, 6]
# Dict iteration
[a: 1, b: 2] -> seq({ $.key }) # ["a", "b"]
[a: 1, b: 2] -> seq({ $.value }) # [1, 2]
# Break (seq and acc only)
[1, 2, 3] -> seq({ ($ > 2) ? break ! $ }) # [1, 2]
# Empty collections
[] -> seq({ $ }) # []
[] -> fan({ $ }) # []
[] -> filter({ $ }) # []
[] -> fold(42, { $ }) # 42
[] -> acc(0, { $ }) # []