backends/default design
Design goal
The algebra traits are only useful if some real dense type implements them.
backends/default provides that reference backend by wrapping the concrete
@mutable and @immut types, while keeping those concrete packages free of
any dependency on the experimental algebra layer. It is a backend, not the
centre of the ecosystem: generic algorithms depend on the traits, and this
package is one way to satisfy them.
Mathematical background
Instances are evidence of laws
Implementing @algebra.MatMulMatrix for a type claims the laws listed in the
algebra design: associativity and distributivity of * over
+ wherever defined. For the wrappers these laws are inherited from the
wrapped types, because every operator is defined as “unwrap, apply the inner
operator, wrap”:
So wrap is a homomorphism for every operation, and any equation that holds
for the inner type holds for the wrapper.
Partiality of the dense product
For runtime-shaped dense matrices the product is defined on
. The trait
method returns Self, so outside that set the wrapper must do something; it
aborts, as the inner type does. This is the documented runtime precondition
that the MatMulMatrix contract asks implementations to state.
Dot product and its rounding error
dot computes by the recurrence
, . In floating point each step multiplies
the error of all earlier terms by another factor , and the
standard argument gives
The relative error is therefore small when the terms have the same sign, and
can be large when (cancellation). matvec is
such dot products and inherits the same bound row by row.
Design decisions
Owned wrapper types
Problem. MoonBit allows impl Trait for Type only in the package that owns
the trait or the type. Implementing @algebra.MatMulMatrix for
@mutable.Matrix would have to happen in algebra (which must not know
concrete types) or in mutable (which would then depend on the experimental
algebra layer).
Decision. Define new types DenseMatrix[T], DenseVector[T] and their
immutable counterparts in this package, each a struct with one public field
inner, and implement the traits for them here.
Why. The wrapper is owned by the package that implements the traits, so
the rule is satisfied, and the dependency direction stays
backends/default → algebra, immut, mutable. The cost is one indirection in
the type, removed by inner() and from_backend without copying.
Backend methods for scalar-valued maps
dot, scale, axpy and matvec are methods of the wrappers, not traits:
they involve the scalar type explicitly, which the algebra traits cannot name
(see the algebra design). Keeping them as methods lets each backend choose its
own constraints, for example AddMonoid + Mul for dot on the mutable vector
and Zero + Add + Mul on the immutable one.
axpy returns a new vector
The BLAS routine axpy updates in place. Here
x.axpy(a, y) returns as a new vector for both wrappers, so the
mutable and immutable backends share one value-returning contract. In-place
updates remain available on the inner @mutable.Vector.
Scalars multiply on the right
scale uses right_scale, computing . For commutative scalars this is
the same as ; for non-commutative scalars the choice is visible, and it
is documented rather than hidden.
Correctness and invariants
- Homomorphism.
inner(a op b) == inner(a) op inner(b)for every operator, so the wrappers satisfy exactly the laws of the wrapped types. - No copies on wrapping.
from_backend(x).inner()is physicallyx; writes to a mutable inner value are visible through the wrapper. - Transpose.
transposematerializes; it never returns a view, so theTransposeMatrixlaw holds as values. - Complexity.
+,-,scale: ;dot: multiply-adds;matvec: ;*: multiply-adds;transpose: copies.
Alternatives rejected
- A runtime backend selector inside one matrix type. It would make every operation branch on the backend and hide which kernel runs. Backends are chosen by type.
- Implementing the traits in
immutandmutabledirectly. That would tie the stable concrete packages to the experimentalalgebralayer. - In-place
axpy. It would give the two wrappers different semantics for the same name.
Boundaries
backends/default defines no new traits and no new numerical algorithms; the
decompositions, inverses and statistics of mutable are
reached through inner(). It does not provide sparse, lazy, static-size or
GPU backends; those should implement the algebra traits for their own types
rather than convert into these wrappers. The native OpenBLAS backend that once
sat beside it is withdrawn in this release and preserved in
contrib/openblas_backend.