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.

Those array operators keep the same name in both call styles. A direct call such as A.map(data, transform) runs immediately. Its data-last pair, A.map(transform)(data), is the shape used by pipe and recognized by the compiler. The complete dual-call catalogue lists all 491 core pairs.

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.