Skip to content

Fusion

pipe and flow run each step in order. Nothing fuses at runtime by itself.

Fusion is a separate, optional build step: @stopcock/fp-compiler, a Vite/Rollup/esbuild plugin. It recognizes a pipe/flow/compile call built from supported array operators and replaces it with one inlined loop before your code ships.

Without the plugin, or for anything it doesn’t recognize, this call runs three full passes over data:

pipe(
data,
A.filter((x: number) => x > 0),
A.map((x: number) => x * 2),
A.take(10),
)

With the plugin, the same call compiles to roughly:

const output: number[] = []
for (let i = 0; i < data.length; i++) {
const x = data[i]
if (!(x > 0)) continue
output.push(x * 2)
if (output.length === 10) break
}

One pass, no intermediate arrays, and take(10) stops the loop as soon as it has 10 results. pipe, A.filter, A.map, and A.take never get called at runtime for a fully-lowered site like this one.

The compiler recognizes chains built only from its supported operators: element steps (map, filter, reject, filterMap, mapWhile, flatMap, take, takeUntil, drop, takeWhile, dropWhile), full-array boundaries (sort, sortBy, reverse, uniq, tail, init, flatten, scan, without), and terminals (sum, count, reduce, forEach, find, findIndex, findMap, every, some, none, head, last, length, isEmpty, join, min, max).

A materializing operator like sort still needs the whole array before it can produce anything, so a chain that includes one compiles to two loops: whatever comes before it fused into the first, then the sort, then whatever comes after:

pipe(
data,
A.filter((x: number) => x > 3), // fused into loop 1
A.map((x: number) => x * 2),
A.sortBy((a: number, b: number) => a - b), // runs on the materialized result
A.take(3), // exact semantics: runs after the complete sort
)

compilePure(A.sortBy(compare), A.take(k)) may replace that exact adjacent pair with a bounded top-k rewrite, but only when you explicitly promise the comparator is pure. Ordinary pipe and compile never do this on their own.

An unsupported operator, a dynamic step, a lexically shadowed import, or anything the compiler can’t resolve statically leaves the site untouched: it’s an ordinary runtime call to pipe, exactly as if the plugin weren’t installed. Nothing breaks, it’s just not fused. Run stopcock check --strict to see which call sites compiled, which bailed, and why.

Compiling never changes results, but it can change how many times a callback runs and in what order if your callbacks have side effects. See the @stopcock/fp-compiler README for the exact interleaving contract, and Benchmarks for how compiled chains compare to lodash, ramda, remeda, rambda, and ts-belt.