Semantics and sharp edges
Everything on this page is by design, and every one of them can surprise you. Read this before putting FArray in a hot path; it is short on purpose.
Fused lambdas are assumed pure
The fusion optimizer reorders, deduplicates, sinks past filters, and deletes computations whose
results nothing reads. Pure lambdas cannot observe any of that; lambdas with side effects can. A
counter inside a map may run fewer times than the input has elements (dead-column elimination),
more times than you expect (no common-subexpression guarantee across pattern branches), or after a
filter you wrote it before (sinking). This is the same contract database optimizers put on
expressions, and it is not checked by the compiler: a logger inside a fused stage is legal Scala
and a semantic bug.
Eager operations (everything without .fuse) run your lambda exactly once per element, in order,
like the standard library.
Lazy structural nodes pin their source
take, drop, slice and reverse are O(1) because they return a node pointing at the source
array. That reference is permanent: hundredMegabytes.take(2) keeps all hundred megabytes
reachable, the same trap String.substring was before JDK 7. When you keep a small slice of a
large FArray long-term, break the tie by materializing: any elementwise operation
(slice.map(identity)) or FArray.from(slice) copies the two elements into fresh storage and
drops the reference.
Safe to share across threads
An FArray (and an FSet) is immutable, and you can publish one and read it from many threads at once
without synchronizing. The only shared mutable state is the cache a lazy node uses so it doesn't
re-materialize itself: the first traversal that needs a flat leaf computes it and stores it in a
plain field, with no lock and no volatile. That store is a benign data race — the
String.hashCode idiom — safe because the two things that make that idiom safe both hold here: the
cached value is an immutable leaf whose fields are final (so any thread that reads the reference
sees a fully constructed value), and every thread computes the same leaf, so a lost race only
costs a little redundant work. The result is always correct. Put an FArray in a shared cache and
read it concurrently; nothing tears.
Reference identity differs where copying was avoided
A filter that keeps everything returns the same object, not a copy. map on an empty FArray
returns the empty singleton. Code that relies on eq-distinctness of collection results (rare, but
it exists) will see sharing the standard library doesn't do.
Binary compatibility does not exist here
Every operation is inline: the dispatch, and often the surrounding logic, expands into your
compiled call sites. Upgrading FArray without recompiling everything that uses it is not supported
and never will be; there is no MiMa story for an inline API. Pin an exact version. If you publish a
library whose public API exposes FArray, your downstream users inherit the same rule.
Compile-time cost
.fuse runs a macro per terminal call: the whole pipeline is parsed off the typed tree, optimized,
and re-emitted. Measured on a file of forty 5-stage pipelines (incremental recompile, warm build
server, Apple Silicon): the fused file compiles in ~0.85s, the identical eager file in ~1.4s, about
20ms per fused pipeline against 36ms per eager one. The direction surprised us too: eager chains
pay an inline kind-dispatch expansion per stage per call site, while fused stages are inert markers
and the one macro per terminal costs less than the five expansions it replaces. Expect fused code
to compile at worst comparably to the eager code it replaces; if you find a pipeline shape where
that inverts badly, that is a cliff worth an issue.