Skip to main content

Building FArrays

Construction is the one place this library starts a step behind by definition: a raw IArray(1, 2, 3) is one array allocation, and an FArray is that array plus its leaf node. That gap is accepted: the design principle says losing to a bare array on inherently-allocating ops is fine, losing to the wrapper libraries (List, Vector, and the Chunk types) is not. This page is how the entry points hold that line.

FArray(…) is a macro, and that matters

FArray(a, b, c) looks like the varargs constructor every library has, and that's exactly the problem it avoids. A normal apply(as: A*) receives its arguments as a Seq[A] (Scala's generic read-only sequence interface): the compiler wraps your three literals into an array, boxes each one if it's a primitive, and hands the constructor a sequence to iterate. You pay a wrapper, N boxes, and a loop to build a three-element collection.

FArray's apply is inline, and it forwards to a macro that never lets the Seq form. The macro reads the varargs literal straight off the syntax tree and unrolls it: it counts the arguments, dispatches on the static element type to pick the concrete leaf kind, and emits the stores directly:

FArray(a, b, c) // what you write, a, b, c: Int

{ val out = new Array[Int](3) // what the macro emits: a typed array,
out(0) = a // unrolled stores, no Seq, no boxing,
out(1) = b // then the leaf constructor directly
out(2) = c
new IntArr(out, 3) }

The element-type dispatch does the work: Int args land in an IntArr over an int[], String args in a RefArr, and the primitives are never boxed because they go straight into the typed array. The degenerate sizes are special-cased in the same pass (no args emit Empty, one arg emits One(a)), so there is no array or loop for the trivial cases either. Every FArray(...) call site gets its own unrolled construction; there is no shared generic varargs path anywhere.

That last point shows against the field in a method constructing collections of several element kinds. A shared generic varargs constructor sees Int, Long, Double, String flow through one call site and boxes them all; FArray emits four independent typed constructions, so there's no shared site to go megamorphic:

measuring…

The constructor family

Beyond literals, the ways to make an FArray split into two groups. From data you already have: from/fromIterable (copy an existing collection into a leaf once), fromArray (wrap an Array kind-exactly), and fromBoxedArray (the escape hatch for an Array[Object] out of reflective code). From a rule: fill(n)(elem), tabulate(n)(i => …) (element from its index), iterate(start, n)(f), and unfold(seed)(f).

tabulate and fill carry one narrow trick. A reference FArray is normally backed by Object[], which makes a later toArray[String] a checked per-element copy rather than a bulk arraycopy. So these two constructors summon a ClassTag (a runtime class token that survives erasure) opportunistically: when one is in scope for the element type, the backing array is allocated as the real String[] (Trade[], …), cast to Object[] only in the static type but still a String[] at runtime, so a later toArray is a straight arraycopy. When no tag is available nothing is demanded of the caller; you keep Object[] and the slower exit.

The boundary is narrow, and easy to over-read: this trick is an island. Every transformation (map, filter, collect) produces a plain Object[]-backed result, so xs.map(_.toString).toArray[String] gets the checked copy regardless. The typed backing survives only from a tabulate/fill (or a fromArray you handed a typed array) straight to toArray with no transform in between. It's a real win on exactly that path and costs nothing elsewhere; it is not a general property of reference FArrays.

Construction is inherently allocating, so the honest baseline is IArray — a raw array, no wrapper. fill ties it; tabulate is the surprise:

measuring…

range is not a loop

FArray.range(0, n) allocates nothing per element: it's a lazy RangeNode, the arithmetic sequence as a tree leaf. Reads compute start + i * step; the range only becomes a real array if something forces it. A pipeline that maps over a range never pays for the range:

measuring…

Accumulating element by element

For the imperative case (pushing elements one at a time when you don't know the count up front), scala.collection.mutable.Builder is the wrong tool: its addOne(elem: A) is generic, so every primitive boxes at the interface. FArray has a dedicated unboxed builder that avoids the boxing and beats even the standard library's specialized int[] builder.