Principles
Conversion lives at the rim
The core computes in f64. Every other numeric shape converts once, on the way in.
The core computes in f64, monomorphically. Every other numeric shape
converts exactly once, at ingestion, and never again.
Why#
The tempting design is a core generic over a Float trait. Accept f32,
integers, decimals, anything, all the way down. The cost shows up everywhere
this library actually earns its keep. Tick placement needs literals (10.0,
0.5, the SI thresholds). Exact-decimal formatting needs a concrete
mantissa. The KDE needs constants. Every generic bound infects every
signature. The hot loops either monomorphize into code-size bloat, or they
dispatch per point. Topos, this library's sibling, pays a documented
"generic-literal gap" tax for payload genericity, because one engine over
scalars and tensors is its literal thesis. Malevich has no such thesis. A
generic float core would buy nothing. It would cost literals, tick math, and
formatting everywhere.
The other temptation is to accept nothing but &[f64]. That taxes every
caller with a conversion loop and an allocation they have to remember to
write, and half of them convert wrongly at the first NaN.
The idea#
The boundary is a trait. The interior is a type. Anything series-shaped —
slices, arrays, vectors of any primitive numeric type, iterators — converts
exactly once, at the rim, into contiguous f64, where NaN is the gap.
A borrowed f64 slice crosses zero-copy. Inside the rim there is one numeric
world. Monomorphic f64, no bounds, no dispatch. Literals and constants are
used freely. The hot loops are code the optimizer can see through.
The rim is also where the ecosystem plugs in without becoming a dependency.
ndarray's contiguous arrays borrow zero-copy, behind a feature. Polars needs
no feature at all, because a contiguous column is already a borrowed slice,
and its null-yielding iterator maps straight onto the gap convention. The
convention is the interface. A big data library integrates by meeting f64
plus NaN, not by being linked.
f64 is the right single type. It holds every f32, every u32, and every
count exactly, and a chart raster cannot resolve the difference that remains
at u64 extremes.
Consequences#
- Ticks, formatting, statistics, and rasterization each have one implementation. There are no generic variants to test in every width.
- The cost of ingestion is explicit, bounded, and paid once. A render loop never converts.
- The gap convention is universal, because ingestion is what establishes it.
After the rim, code may assume
NaNmeans gap. - A new input shape is an
IntoSeriesimplementation, not a change to the core. - No
Floatbound ever appears in a public signature.
Not this#
- The core is not generic over a float trait, and a draw loop does not call
Into<f64>per point. - There is no second numeric path for
f32"for performance." - Accepting a dataframe's columns does not mean depending on that library.
- Stats do not convert lazily. The same series does not pay on every use.
See What earns a concept for the closed core in general, and Vision rule 2.
Spelled today#
data::Series is the contiguous f64 column; data::IntoSeries is the
rim, with zero-copy borrowing for f64 slices and FromIterator for
iterators. The ndarray feature adds zero-copy ingestion for contiguous
arrays; the polars recipe in the README is two lines against the public
rim. Function sampling arrives with the marks, not through the rim. This
section may rot; the rest must not.