immut/dense design
Design goal
DensePolynomial[A] is the univariate polynomial as a value: a canonical coefficient vector that can be shared freely, compared with ==, and combined with the ring operators. It targets polynomials whose coefficients are mostly non-zero, where storing every coefficient up to the degree is both the simplest and the fastest layout.
Mathematical background
A univariate polynomial over is a finitely supported sequence , written . Its degree is , with (returned as None). Addition is coefficient-wise and multiplication is the Cauchy product
If is a commutative ring then so is ; if is only a semiring (such as UInt) then is a semiring. The degree satisfies
with equality in the second exactly when the product of the leading coefficients is non-zero, which always holds when has no zero divisors.
Design decisions
Canonical form: trim trailing zeros
Problem. The sequences and describe the same polynomial. If both could be stored, ==, degree, length and hashing would all have to normalize first.
Choice. Every constructor and every operation trims trailing zeros before returning, and the zero polynomial is the empty vector. Trimming defines a bijection between polynomials and words with (plus the empty word), so
Trimming after every operation, not only after construction, matters because the degree inequalities above can be strict. In Int, which is , : the leading coefficient vanishes, and the trimmed result correctly has degree .
The vector is a persistent @immut/vector.Vector, and from_coefficients copies its input, so a caller mutating its array afterwards cannot change a polynomial.
Minimal coefficient bounds per operation
Problem. One bound such as A : Ring on the whole type would exclude useful coefficient types: UInt has no negation, and some types have multiplication without a unit.
Choice. Each function states the smallest set of luna-generic capabilities it uses. Addition needs Eq + AddMonoid (Eq and Zero to trim), multiplication adds Mul but not One, negation needs Neg but not Mul, and only variable, one, pow and karatsuba need One. The type itself has no bound, so zero(), length() and degree() work for any A.
Schoolbook multiplication by default
* computes the Cauchy product with two nested loops over the stored coefficients, coefficient multiplications and additions for lengths and , then trims. For the short polynomials that dominate typical use this is faster than any recursive method, needs no Neg, and is exact for exact coefficient types.
Karatsuba as an explicit method
Problem. For long operands, becomes the bottleneck.
Derivation. Split each operand at : and with and . Then
where the second line is distributivity alone, so it holds over any ring. Three half-size products replace four. With the cost for length and for the additions and shifts,
Choice. karatsuba splits at half the longer length, recurses on the three products, and falls back to * when the shorter operand has at most coefficients, where the recursion overhead outweighs the saved multiplications. The subtraction needs Neg, and the shift by is scale(k, One::one()), which needs One. It is a separate method rather than the implementation of * so that * keeps its weaker bounds and its predictable cost; a test checks that both agree above the threshold.
Horner evaluation
is computed as
one multiplication and one addition per stored coefficient, starting from zero. Horner’s rule uses the fewest multiplications possible for evaluating a general polynomial without preprocessing its coefficients.11 Ostrowski proved the optimality for degree at most 4 (1954) and Pan for every degree (1966): any algorithm evaluating a generic polynomial of degree needs at least multiplications and additions. It needs only AddMonoid + Mul, so it evaluates over any coefficient type, and it never forms the powers , which keeps intermediate values small for fixed-width integers and well-conditioned for floating point.
Composition is evaluation at a polynomial
substitute(q) runs the same Horner loop with polynomial arithmetic, computing . For commutative this is the evaluation homomorphism , , the unique ring homomorphism fixing and sending to . On monomials,
and both sides are bilinear, so and . Composition is associative, , because both sides are homomorphisms that agree on .
With and , step of Horner multiplies a polynomial of degree by , so the schoolbook cost is , and the result has degree at most .
Formal derivative through the canonical map from ℕ
derivative computes , mapping the integer into with NatHomomorphism::from_nat. is additive and satisfies the Leibniz rule. On monomials,
and both sides of are bilinear in , so the rule extends to all polynomials. Because is taken modulo the characteristic, in characteristic , exactly as in algebra. luna-generic provides NatHomomorphism for Float, Double and BigInt; fixed-width integer coefficients have no derivative until they get such an instance (the trait is being replaced by FromNat upstream).
Powers by binary exponentiation
pow(e) delegates to the package-internal pow_nat, which keeps the invariant while halving exp. It performs squarings and at most one multiplication into the state per set bit of , so at most polynomial multiplications. Since the operands grow, the last squaring dominates: with schoolbook multiplication the total cost is .
pow(0) returns one() for every , including zero, so . This is the convention of @arithmetic.PowNatChecked, which DensePolynomial implements by returning Ok(self.pow(e)).
A structural order for containers
The derived Compare orders by stored length, then coefficient by coefficient from the constant term. Because of trimming, length is degree plus one, so lower-degree polynomials sort first. The order exists so that polynomials can be keys of sorted maps and sets; it is not a ring order and is not compatible with + or *.
Correctness / invariants
- Canonical form. After every public operation the last stored coefficient is non-zero, and zero is empty. Equality is therefore polynomial equality.
- Ring laws. When the coefficient type satisfies the commutative-ring laws,
DensePolynomial[A]does: the operations are the textbook formulas on canonical forms. Property tests check the additive identity and idempotent canonicalization. - Agreement.
karatsuba(a, b) == a * b;substituteis ;derivativesatisfies the Leibniz rule;pow(e)equals the -fold product. - Value semantics. No method mutates its receiver or its arguments; constructors copy input arrays.
- Complexity for lengths :
+,-,neg,scale;*;karatsubafor balanced operands above the threshold;eval;substitute;pow(e)multiplications.
Alternatives rejected
- Karatsuba inside
*. It would forceNeg + Oneon every multiplication and change the cost profile of short products. Keeping it explicit leaves the choice to the caller. - FFT or number-theoretic multiplication. These need roots of unity or a suitable modulus in the coefficient type, which a generic
Adoes not provide. - Storing the degree separately and allowing trailing zeros. It saves a trim on some operations but makes equality and hashing depend on normalization; trimming once per operation is cheap and removes the question.
- A sparse univariate type. Polynomials such as waste space here; use
SparsePolynomialwith one-element exponent vectors for them.
Boundaries
- Univariate only. Multivariate polynomials are
TermPolynomialandSparsePolynomial. - No division, GCD, factorization, root finding or interpolation.
- Exact trimming: a coefficient is dropped only when it
==zero. WithFloatorDoublecoefficients a leading coefficient that should cancel but carries rounding error is kept, so the stored degree can exceed the mathematical one. - Fixed-width integer coefficients wrap, so the polynomials are over , with zero divisors and degree drops as shown above.
powtakes aUIntexponent; there are no negative powers.
Footnotes
-
Ostrowski proved the optimality for degree at most 4 (1954) and Pan for every degree (1966): any algorithm evaluating a generic polynomial of degree needs at least multiplications and additions. ↩