Iterators and streams
Yona is strictly evaluated, but two library types give you streaming,
demand-driven data processing: the prelude Iterator (a stateful pull
handle, ideal for I/O sources) and Std\Stream (a pure lazy sequence built
from explicit thunks). Both let you process data far larger than memory — one
element resident at a time.
The Iterator type
Section titled “The Iterator type”Iterator is a prelude type — available everywhere without an import:
type Iterator a = Iterator (() -> Option a)An iterator wraps a next function: each call returns Some element until the
source is exhausted, then None. The state (file offset, scan position) lives
behind the closure, so iterators are inherently single-use — once drained,
they cannot be rewound.
Streaming sources in the stdlib
Section titled “Streaming sources in the stdlib”Several stdlib functions return iterators instead of materialized sequences:
| Function | Returns | Yields |
|---|---|---|
Std\File::readLines path |
Iterator String |
file lines, 64 KB buffered |
Std\String::chars str |
Iterator Int |
character codes |
Std\String::split delim str |
Iterator String |
substrings, on demand |
Std\String::lines str |
Iterator String |
lines split on \n |
import chars, split from Std\String inlet codes = [c for c = chars "hi"], parts = [s for s = split "," "a,b,c"]in codes# => [104, 105] (parts is ["a", "b", "c"])Generators consume iterators in O(1) memory
Section titled “Generators consume iterators in O(1) memory”Comprehensions detect an Iterator source and compile to a streaming loop: call
next(), stop on None, evaluate the body on the element, append to the
result. Only one source element is live at a time.
import readLines from Std\File, length from Std\String in[length line for line = readLines "large_file.txt"]# => one Int per line — the file is never fully residentImplementation note. The generator loop appends with an O(1)-amortized
seq_snoc, so results grow without a size limit. readLines is backed by a C
iterator holding a 64 KB read buffer that is reused across next() calls;
memory use is O(64 KB) regardless of file size. The result sequence is
materialized — if you also want the output to stay small, fold instead of
collecting (see the worked example below).
Iterators vs materialized sequences
Section titled “Iterators vs materialized sequences”| Scenario | Seq (eager) |
Iterator (streaming) |
|---|---|---|
| 50 MB file, count lines | O(50 MB) memory | O(64 KB) memory |
| 1M-char string, per-char work | O(1M) allocations up front | O(1) per char |
| Split a 10K-field CSV row | 10K strings up front | one string per field |
Guidance: use a Seq when the data is small, when you need random access,
length, or multiple passes. Use an iterator when the source is I/O, when the
data may be large, or when you will consume it exactly once, front to back.
Iterators are forward-only, have no length, and are single-use.
Std\Stream: lazy sequences from explicit thunks
Section titled “Std\Stream: lazy sequences from explicit thunks”Iterator hides mutable state in the runtime. Std\Stream is the pure
alternative: laziness encoded directly in an ADT, with the “rest of the
sequence” as an explicit thunk. This is how a strict language expresses lazy
streams — the same shape as OCaml’s Seq or ML lazy streams:
type Stream a = Yield a (() -> Stream a) | NilYield x rest exposes the head element and a function that produces the rest
when called. Nothing runs until a consumer forces the next step. There is no
hidden state: “what comes next” lives in the recursive arguments of whatever
operator built the stream.
Producers
Section titled “Producers”empty, singleton, fromSeq, range, naturals, repeat, iterate,
unfold, and fromIterator start a pipeline:
import range, iterate, unfold from Std\Stream inrange 1 5 # 1, 2, 3, 4 (hi is exclusive)iterate (\n -> n * 2) 1 # 1, 2, 4, 8, ... (infinite)unfold (\s -> if s > 3 then None else Some (s, s + 1)) 1 # 1, 2, 3repeat, iterate, and naturals are infinite — always bound them with take
or a short-circuiting terminator before materializing.
Lazy transformers
Section titled “Lazy transformers”map, filter, take, drop, takeWhile, dropWhile, zip, zipWith,
concat, flatMap, scan, and chunksOf transform a stream without running
it. Pipelines read naturally with |>:
import fromSeq, map, sum from Std\Stream infromSeq [1, 2, 3] |> map (\x -> x * x) |> sum# => 14import range, filter, take, toSeq from Std\Stream inrange 1 1000000 |> filter (\x -> x % 7 == 0) |> take 3 |> toSeq# => [7, 14, 21] (the range is never fully evaluated)chunksOf n groups consecutive elements into Seq chunks of size n (the last
chunk may be shorter) — here [1, 2, 3], [4, 5, 6], [7]:
import range, chunksOf, count from Std\Stream inrange 1 8 |> chunksOf 3 |> count# => 3Terminators
Section titled “Terminators”toSeq, foldl, forEach, count, sum, anyMatch, allMatch, find,
head, and isEmpty actually pull elements through the pipeline. anyMatch,
allMatch, find, and head short-circuit:
import naturals, map, find from Std\Stream incase naturals |> map (\n -> n * n) |> find (\sq -> sq > 50) of Some sq -> sq None -> 0end# => 64Resource scoping: bracket Partial
Section titled “Resource scoping: bracket Partial”bracket acquire release produce runs release exactly once when the stream
from produce is fully drained; the resource is held across the whole stream,
not per element:
import bracket, forEach from Std\Stream inbracket (\_ -> openThing 0) (\r -> closeThing r) (\r -> streamFrom r) |> forEach handleThe partial part: abandoning a bracketed stream before Nil (for example
take 10 of a longer source) currently leaks the resource — a consumer-drop
signal is planned. acquire and release take an ignored Int argument rather
than () for calling-convention reasons.
Concurrency boundaries
Section titled “Concurrency boundaries”An entire Std\Stream pipeline runs in the consumer’s task. Forcing the next
element is a function call, with no hidden thread or channel. Streams contain
arbitrary continuation closures, so they are intentionally not Shareable and
cannot safely cross Std\Task.spawn. For concurrent pipelines, compose
Std\Channel and Std\Task directly and send only values whose types satisfy
the required Send and Shareable constraints.
Worked example: a large file in constant memory
Section titled “Worked example: a large file in constant memory”Total the line lengths of a file without ever holding more than one line (plus the 64 KB read buffer) in memory. The comprehension streams from the iterator and the fold consumes each element as it arrives:
import readLines from Std\File, foldl from Std\List, length from Std\String infoldl (\total n -> total + n) 0 [length line for line = readLines "lines.txt"]printf 'alpha\nbeta\ngamma\n' > lines.txtyonac -o total total.yona./total# => 14The same shape with Std\Stream keeps everything in one lazy, sequential
pipeline:
import fromIterator, map, sum from Std\Stream, readLines from Std\File, length from Std\String infromIterator (readLines "lines.txt") |> map (\line -> length line) |> sum# => 14(The lambda wrapper around length is currently required — passing an imported
function directly as a higher-order argument is a known compiler gap.)
Limitations
Section titled “Limitations”- Iterators are linear. Forward-only, no
lengthwithout draining, single-use. Wrapping the same iterator withfromIteratortwice yields two streams that share and corrupt state — lift each iterator exactly once. - Streams are single-consumer.
toSeqdrains the stream; a second consumer would re-run the pipeline from scratch. A broadcast primitive is planned separately. - No stream fusion for
Std\Streamyet. Eachmap/filterstep allocates a closure per element. Comprehension pipelines are fused (see Performance); for the hottest sequential loops, prefer a comprehension or a singlefoldlover a long stream pipeline. - Dict/Set iteration is not yet exposed as an iterator.
ziptermination.zipstops when either input ends; the other stream’s producer is left dangling (same root cause as thebracketabandonment gap).