Symbols and Numbers
Topic:
core.symbols-numbers
Symbols represent mathematical variables (x, y, θ, etc.) using efficient string interning. Numbers support integers, rationals, floats, and complex numbers with exact symbolic representation for precise mathematical computation.
Symbols and Numbers
This chapter covers the two fundamental building blocks of expressions: symbols (variables) and numbers.
Symbols
Symbols represent mathematical variables like , , , etc.
Creating Symbols
#![allow(unused)] fn main() { use mathhook::prelude::*; let x = symbol!(x); let y = symbol!(y); let theta = symbol!(theta); }
Symbol Equality
Symbols with the same name are considered equal:
#![allow(unused)] fn main() { assert_eq!(symbol!(x), symbol!(x)); assert_ne!(symbol!(x), symbol!(y)); }
String Interning
MathHook uses string interning for symbols, making equality checks O(1) pointer comparisons.
Why This Design?
Why String Interning for Symbols?
Design Decision: Symbol names are stored in a global intern table, with symbols holding only a reference.
Why?
- Fast equality: Comparing two symbols is a single pointer comparison (O(1))
- Memory efficiency: Symbol name "x" stored once, shared by all
symbol!(x)instances - Cache-friendly: Symbols are just pointers (8 bytes on 64-bit systems)
Without Interning: Every symbol!(x) would store its own copy of "x" and require string comparison (O(n))
Trade-off: Global mutable state for intern table
- Thread-safe using locks or lock-free data structures
- One-time cost on first use of each symbol name
- Benefit far outweighs cost (10-100x faster symbol comparison)
Example:
#![allow(unused)] fn main() { let x1 = symbol!(x); let x2 = symbol!(x); // Same pointer internally - O(1) comparison assert_eq!(x1, x2); }
When This Matters:
- Pattern matching with many symbol comparisons
- Substitution operations
- Expression equality checking
- Hash table lookups
Numbers
MathHook supports multiple number types for different use cases.
Integers
Arbitrary precision integers for exact computation:
#![allow(unused)] fn main() { let n = Expression::integer(123456789); let large = Expression::integer(9999999999999999999); // Arbitrary precision }
Rationals
Exact representation of fractions:
#![allow(unused)] fn main() { let frac = Expression::rational(22, 7); // 22/7 ≈ π let half = Expression::rational(1, 2); // 1/2 // Always in reduced form let six_fourths = Expression::rational(6, 4); // Automatically becomes 3/2 }
Floats
Floating-point numbers for approximate computation:
#![allow(unused)] fn main() { let pi_approx = Expression::float(3.14159265359); let e_approx = Expression::float(2.71828182846); }
Warning: Use floats only when approximation is acceptable. Prefer rationals for exact arithmetic.
Why Rational Numbers Over Floats?
Design Decision: Exact Rational Arithmetic
Why MathHook Uses Rationals for Symbolic Math:
The Problem with Floats:
#![allow(unused)] fn main() { // Using floats (WRONG for symbolic math) let third = 0.33333333; let result = 3.0 * third; // Result: 0.99999999 (imprecise) // Using rationals (CORRECT for symbolic math) let third = Expression::rational(1, 3); let result = expr!(3 * third); // Result: 1 (exact) }
Why?
- Mathematical correctness:
1/3is exactly1/3, not an approximation - Symbolic operations: Algebra requires exactness (cannot lose precision)
- Accumulation prevention: No rounding error buildup
- Comparison reliability: Exact equality testing
When We Use Floats:
- Only for numerical approximation (explicit
.evalf()) - Only when exact representation is impossible (e.g., transcendental results)
- NEVER in symbolic operations
Real-World Example:
#![allow(unused)] fn main() { // Solving x^2 - 2 = 0 symbolically let x = symbol!(x); let eq = expr!((x ^ 2) - 2); let solutions = eq.solve(&x); // Solutions: [-√2, √2] (exact symbolic form) // NOT: [-1.414213562, 1.414213562] (approximate floats) }
Alternative Considered: Always use floats (like numerical libraries)
- Pros: Simpler implementation, predictable memory usage
- Cons: Catastrophic for symbolic algebra (precision loss, equality breaks)
- Decision: Exact arithmetic is non-negotiable for CAS
Why This Matters:
- Computer algebra requires exactness by definition
- SymPy and Mathematica use same approach
- Prevents subtle bugs from rounding errors
- Enables reliable symbolic simplification
Performance Impact:
- Rational arithmetic is slower than float (2-10x)
- Acceptable trade-off for correctness
- Use
.evalf()when you need speed and can tolerate approximation
Why 16-Byte Number Type?
Design Decision: The Number type is exactly 16 bytes.
Why?
- Cache efficiency: Two numbers fit in a 32-byte expression
- Tagged union: Discriminant + data in 16 bytes
- Balance: Small enough for cache, large enough for pointer + metadata
Structure:
[1 byte: type tag] [15 bytes: data]
- Integer: pointer to BigInt (8 bytes) + padding
- Rational: two pointers to BigInt numerator/denominator (need clever packing)
- Float: f64 (8 bytes) + padding
- Complex: pointer to ComplexData (8 bytes) + padding
Trade-off: Arbitrary precision requires heap allocation
- Small integers (i64) could fit inline, but design consistency favors uniform handling
- Large integers/rationals use
BigInton heap (pointer stored in 16 bytes)
Alternative Considered: Variable-size numbers
- Pros: i64 could be inline (faster)
- Cons: Variable size breaks expression size constraint
- Decision: Uniform 16-byte size maintains expression size guarantee
Complex Numbers
Complex numbers with real and imaginary parts:
#![allow(unused)] fn main() { // 3 + 4i let z = Expression::complex( Expression::integer(3), Expression::integer(4) ); // Or using addition let z = expr!(3 + (4 * Expression::i())); }
Number Operations
Arithmetic
#![allow(unused)] fn main() { let a = Expression::integer(2); let b = Expression::integer(3); let sum = expr!(a + b); // 5 let product = expr!(a * b); // 6 let power = Expression::pow(a.clone(), b.clone()); // 8 }
Exact vs Approximate
#![allow(unused)] fn main() { // Exact: Use rationals let exact = Expression::rational(1, 3); let tripled = expr!(exact * 3); // Result: 1 (exact) // Approximate: Use floats let approx = Expression::float(0.333333); let tripled_approx = expr!(approx * 3.0); // Result: 0.999999 (approximate) }
Type Conversions
To Float
#![allow(unused)] fn main() { let rational = Expression::rational(1, 3); let as_float = rational.to_float(); // 0.333... }
To Rational
#![allow(unused)] fn main() { let float = Expression::float(0.5); let as_rational = float.to_rational(); // 1/2 (if representable) }
Mathematical Constants
Pre-defined constants are available:
#![allow(unused)] fn main() { let pi = Expression::pi(); // π let e = Expression::e(); // e let i = Expression::i(); // i (imaginary unit) let phi = Expression::golden_ratio(); // φ = (1 + √5) / 2 let gamma = Expression::euler_gamma(); // γ (Euler-Mascheroni constant) }
Next Steps
Examples
Symbol Creation and Equality
Creating symbols with string interning for O(1) equality checks
Rust
#![allow(unused)] fn main() { use mathhook::prelude::*; let x1 = symbol!(x); let x2 = symbol!(x); let y = symbol!(y); // O(1) pointer comparison assert_eq!(x1, x2); assert_ne!(x1, y); }
Python
from mathhook import symbol
x1 = symbol('x')
x2 = symbol('x')
y = symbol('y')
# Fast equality check
assert x1 == x2
assert x1 != y
JavaScript
const { symbol } = require('mathhook-node');
const x1 = symbol('x');
const x2 = symbol('x');
const y = symbol('y');
// Fast equality check
console.assert(x1.equals(x2));
console.assert(!x1.equals(y));
Exact Rational Arithmetic
Using rationals for exact fractional computation
Rust
#![allow(unused)] fn main() { use mathhook::prelude::*; // Exact: 1/3 let third = Expression::rational(1, 3); let result = expr!(3 * third); assert_eq!(result, Expression::integer(1)); // Auto-reduction: 6/4 = 3/2 let frac = Expression::rational(6, 4); assert_eq!(frac, Expression::rational(3, 2)); }
Python
from mathhook import Expression, expr
# Exact: 1/3
third = Expression.rational(1, 3)
result = expr('3 * third')
assert result == Expression.integer(1)
# Auto-reduction: 6/4 = 3/2
frac = Expression.rational(6, 4)
assert frac == Expression.rational(3, 2)
JavaScript
const { Expression, expr } = require('mathhook-node');
// Exact: 1/3
const third = Expression.rational(1, 3);
const result = expr('3 * third');
console.assert(result.equals(Expression.integer(1)));
// Auto-reduction: 6/4 = 3/2
const frac = Expression.rational(6, 4);
console.assert(frac.equals(Expression.rational(3, 2)));
Complex Numbers
Working with complex numbers and imaginary unit
Rust
#![allow(unused)] fn main() { use mathhook::prelude::*; // 3 + 4i let z = Expression::complex( Expression::integer(3), Expression::integer(4) ); // Magnitude: |z| = sqrt(3^2 + 4^2) = 5 let magnitude = expr!(sqrt((3^2) + (4^2))); assert_eq!(magnitude.simplify(), Expression::integer(5)); }
Python
from mathhook import Expression, expr
# 3 + 4i
z = Expression.complex(3, 4)
# Magnitude: |z| = 5
magnitude = expr('sqrt(3^2 + 4^2)')
assert magnitude.simplify() == Expression.integer(5)
JavaScript
const { Expression, expr } = require('mathhook-node');
// 3 + 4i
const z = Expression.complex(3, 4);
// Magnitude: |z| = 5
const magnitude = expr('sqrt(3^2 + 4^2)');
console.assert(magnitude.simplify().equals(Expression.integer(5)));
Performance
Time Complexity: O(1) for symbol creation/comparison, O(1) for number operations
API Reference
- Rust:
mathhook_core::expression::Symbol, mathhook_core::number::Number - Python:
mathhook.Symbol, mathhook.Number - JavaScript:
mathhook-node.Symbol, mathhook-node.Number