mutable/dense design
Design goal
The mutable DensePolynomial[A] lets an algorithm build or update a univariate polynomial in place (set coefficients one by one, accumulate sums, multiply into a running product) without allocating a new value for every step, while giving exactly the same mathematical results as immut/dense.
Mathematical background
The represented object is the same as in the immutable package: a polynomial stored as its trimmed coefficient word (see the immut/dense design). A mutable container is a variable holding such a value. An in-place operation is the assignment ; it must leave the container holding the canonical word of the new value.
Design decisions
The same canonical form, restored after every mutation
Invariant. Between public calls, the stored array never ends in a zero coefficient.
Each mutating method re-establishes it. set_coefficient(k, c) grows the array with zeros when , writes , and trims: if was written at the top, the degree drops to the next non-zero coefficient. add_inplace adds into the existing array and trims, because cancellation can lower the degree. mul_inplace and scale_inplace assign a freshly computed canonical array. Because the invariant holds, degree, length, == and compare mean exactly what they mean for the immutable type.
Mutation is explicit and limited
Only set_coefficient, clear, add_inplace, mul_inplace and scale_inplace change the receiver. The operators +, -, *, unary - and every other method return new values: a + b copies a and adds b into the copy. Code that reads p * q therefore never has a hidden side effect, and mutation is visible at the call site by its name.
Aliasing is safe
Passing the receiver as the argument is allowed. For p.add_inplace(p), the loop computes index by index; at step both operands read index before it is written, and later steps read only later indexes, so the result is . mul_inplace and scale_inplace compute the whole result before assigning it, so squares correctly.
Delegation to the immutable implementation
Operations that would duplicate non-trivial algorithms (scale, pow, karatsuba, substitute, derivative, monomial_checked) convert to the immutable type, call it, and convert back. Each conversion copies the coefficients, , which is dominated by the algorithm itself. The two layers therefore cannot drift apart on these operations. Addition, schoolbook multiplication and Horner evaluation, which are short, are implemented directly on the array; the consistency tests check that they agree with the immutable ones.
Conversion copies
from_immut copies the immutable coefficients into a new array, and to_immut builds an immutable value from a copy. A snapshot taken with to_immut stays unchanged however the mutable polynomial is modified afterwards, and an immutable value handed to from_immut is never affected by later mutation. copy (the Copyable capability) duplicates the array, so the copy and the original evolve independently.
Costs compared with the immutable type
| Operation | immut/dense | mutable/dense |
|---|---|---|
| set one coefficient | rebuild with from_coefficients, | set_coefficient, (the trim copies) |
| new value, | add_inplace, , reuses the array | |
| , plus one assignment | ||
pow, substitute, karatsuba | direct | the same plus conversions |
Trimming in set_coefficient copies the array, so each call is in the current implementation; the saving over the immutable type comes from avoiding a new polynomial value per step and from add_inplace working on the existing storage.
Correctness / invariants
- Canonical form after every public call;
==is polynomial equality. - Agreement with immut. For every non-mutating operation,
from_immut(a).op(...).to_immut() == a.op(...); in-place operations leave the receiver equal to the corresponding operator result. - Isolation.
copy,to_immut,from_immutandto_coefficientsnever share storage with the receiver. - Aliasing.
p.add_inplace(p),p.mul_inplace(p)compute and . - Aborts.
set_coefficient,scale_inplace,scale,coefficientandmonomialabort on a negative power, like their immutable counterparts.
Alternatives rejected
- Mutating operators. Making
+or*update the left operand would be cheaper in loops but would make every arithmetic expression a potential side effect. - Sharing the array with immutable snapshots. Copy-on-write would make
to_immutbut needs reference tracking that MoonBit arrays do not provide. - A separate algorithm set. Re-implementing Karatsuba or composition for the mutable type would double the code that has to be kept consistent.
Boundaries
- No
set_coefficient_checked; validate the power yourself or use the immutablescale_checked/monomial_checked. - Mutating a polynomial while another computation iterates over its
to_coefficients()result is safe only because that result is a copy. - A mutable container shared between two parts of a program is updated for both; give each part its own
copy()when that is not intended. - Everything that
immut/densedoes not do, this package does not do either.