Sources
the source
A pipeline has to read from somewhere, and fusion's first structural decision is that the engine is a streaming consumer defined against a source, not an array trick. The same stages and terminals compile over three source shapes, and which one you're on is written in the pipeline's type.
The three shapes
An in-memory
FArray:xs.fuse. The degenerate case: the whole collection is one chunk, and the emitted code is the tight indexed loop shown on the previous page.runhands you anFArrayback, so fused pipelines slot invisibly into eager code.A chunked
Source[A]:Source.fromIterator(it).fuse,Source.unfold(z)(step),Source.iterate(x)(f). Pull-based and chunk-granular: one virtualpullChunkcall hands the loop a whole unboxed leaf (~4096 elements), so the hot path stays the same element loop, with no per-element virtual call and no boxed sentinel. Constant memory: the loop holds one chunk, not the data, so a gigabyte input needs kilobytes of heap. And the driver wraps the loop intry/finally close(), so a file or socket behind the source is released on exhaustion, on short-circuit (take/findstopping early), and on exception alike.A record-framed byte source (
ByteRecordSource):Json.ndjson[Rec](bytes).stream: elements decoded straight out of raw bytes, fields the pipeline doesn't read skipped at the byte level. Same stages, same terminals, same optimizer; the details are on the JSON integration page.
The chunked contract is two methods. A source hands back leaves until it's done, and promises a chunk
is only valid until the next pull, which is the license an InputStream-backed source needs to
reuse one read buffer across pulls:
Streaming, measured
Constant-memory processing on the JVM usually costs throughput: Iterator chains and
java.util.stream pipelines pay per-element virtual dispatch, LazyList (Scala's lazily evaluated
linked list) allocates a cons cell per element, effect-system streams pay their runtime.
The chunk-granular design keeps fusion's loop shape over lazy data. The benchmark folds a generated
sequence, and a file's lines, without holding either in memory:
The shape is in the type
The entry point stamps the pipeline: xs.fuse gives Fuse[A, Chunks], and the JSON module's
.stream gives Fuse[T, Ndjson]. That phantom parameter, the source shape, drives the rest of
the machine: stages are shape-generic, but
which terminals exist, and how they lower, is decided per shape
by a typeclass (an interface implementation the compiler selects by type, like a ServiceLoader
resolved at compile time). It has no runtime cost; it exists so that an unsupported operation is a
compile error rather than a runtime surprise.