A general technique is to view your n-dimensional type algebra as a vector space in ℤn, where each vector coefficient corresponds to an exponent of your unit. Your 'basis' of this vector space can be the SI units for example. Then multiplication of units becomes addition in this vector space, and division becomes subtraction. You can store the coefficients of your vector space as template parameters, this way you can ensure that addition/subtraction is implemented between values that have the same unit. To aid with this I would suggest the typenum crate. Finally you can add type aliases to make working with it sensible.
As an example with just Meter, Kelvin and Second, using a f64 as storage:
use core::marker::PhantomData;
use typenum::{Integer, Z0, P1};
#[derive(Clone, Copy)]
struct SIValue<M, K, S> {
val: f64,
unit: PhantomData<(M, K, S)>,
}
type Scalar = SIValue<Z0, Z0, Z0>; // Unitless - zero vector.
type Meter = SIValue<P1, Z0, Z0>; // Basis type vector for meter.
type Kelvin = SIValue<Z0, P1, Z0>; // Basis type vector for Kelvin.
type Second = SIValue<Z0, Z0, P1>; // Basis type vector for second.
fn main() {
let g = Meter::new(9.8) / Second::new(1.0) / Second::new(1.0);
println!("Gravitional constant: {}", g);
let mut dist = Meter::new(0.0);
let mut accel = Meter::new(0.0) / Second::new(1.0);
let mut t = Second::new(0.0);
let dt = Second::new(0.001);
let maxt = Second::new(5.0);
while t < maxt {
accel = accel + dt * g;
dist = dist + accel * dt;
t = t + dt;
}
println!("Distance after falling for {t:.1}: {dist:.1}");
println!("Acceleration after {t:.1}: {accel:.1}");
}
Which prints:
Gravitional constant: 9.8 m s^-2
Distance after falling for 5.0 s: 122.5 m
Acceleration after 5.0 s: 49.0 m s^-1
Given the following implementation (or in the playground):
use core::ops::{Add, Sub, Mul, Div};
use core::fmt;
impl<M, K, S> SIValue<M, K, S> {
fn new(val: f64) -> Self {
Self { val, unit: PhantomData }
}
}
impl<M: Integer, K: Integer, S: Integer> fmt::Display for SIValue<M, K, S> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let write_unit = |f: &mut fmt::Formatter, unit, exp| {
match exp {
0 => Ok(()),
1 => write!(f, " {}", unit),
n => write!(f, " {}^{}", unit, exp),
}
};
write!(f, "{}", self.val)?;
write_unit(f, "m", M::to_i32())?;
write_unit(f, "K", K::to_i32())?;
write_unit(f, "s", S::to_i32())
}
}
impl<M, K, S> Add for SIValue<M, K, S> {
type Output = Self;
fn add(self, rhs: SIValue<M, K, S>) -> Self::Output {
SIValue::new(self.val + rhs.val)
}
}
impl<M, K, S> Sub for SIValue<M, K, S> {
type Output = Self;
fn sub(self, rhs: SIValue<M, K, S>) -> Self::Output {
SIValue::new(self.val - rhs.val)
}
}
impl<LM, LK, LS, RM, RK, RS> Mul<SIValue<RM, RK, RS>> for SIValue<LM, LK, LS>
where
LM: Add<RM>, LK: Add<RK>, LS: Add<RS>,
{
type Output = SIValue<
<LM as Add<RM>>::Output,
<LK as Add<RK>>::Output,
<LS as Add<RS>>::Output,
>;
fn mul(self, rhs: SIValue<RM, RK, RS>) -> Self::Output {
SIValue::new(self.val * rhs.val)
}
}
impl<LM, LK, LS, RM, RK, RS> Div<SIValue<RM, RK, RS>> for SIValue<LM, LK, LS>
where
LM: Sub<RM>, LK: Sub<RK>, LS: Sub<RS>,
{
type Output = SIValue<
<LM as Sub<RM>>::Output,
<LK as Sub<RK>>::Output,
<LS as Sub<RS>>::Output,
>;
fn div(self, rhs: SIValue<RM, RK, RS>) -> Self::Output {
SIValue::new(self.val / rhs.val)
}
}
If you were to try to add the wrong units, you would get an error, like if you were to do accel + dt in the above example:
error[E0308]: mismatched types
--> src/main.rs:17:25
|
17 | accel = accel + dt;
| ^^ expected struct `PInt`, found struct `Z0`
|
= note: expected struct `SIValue<PInt<UInt<UTerm, B1>>, _, NInt<UInt<UTerm, B1>>>`
found struct `SIValue<Z0, _, PInt<UInt<UTerm, B1>>>`
Unfortunately, the error messages aren't particularly good.