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:
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:
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:
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.