I agree. One approach can look like this.
Std code:
// assuming we'll get const trait fns
// here const guarantees that trait have only const methods
trait UnitSystem: const Div + const Mul + const BitXor { }
struct UnitValue<Value, U: UnitSystem, const UNIT: U> {
value: Value,
// not sure if we need PhantomData field here
phantom: PhantomData<..>,
}
impl<V1, V2, U, const U1: U, const U2: U> Mul<RHS=UnitValue<V1, U, U1>>
for UnitValue<V2, U, U2>
where U: UnitSystem, V2: const Mul<V1>
{
type Output = UnitValue<V2::Output, U, U1*U2>;
// ideally we should be able to define `const fn mul` if `Mul<V1>` is const
// and `fn mul` if `Mul<V1>` is not const,
// it's probably should be possible with specialization
// but to start things we can do this impl only for constant `Mul`s
const fn mul(self, rhs: Self::RHS) -> Self::Output {
UnitValue { value: self.value*rhs.value, phantom: Default::default() }
}
}
// generic impls for Div, Add, etc., some inhrent methods for f32, f64, etc.
We also can add helper methods to UnitSystem
trait for conversion between unit systems, but I am not completely sure how they should look.
Unit system crate:
struct SI {
meter: i8,
second: i8,
kelvin: i8,
// ..
}
impl Mul for SI {
type Output = SI;
const fn mul(mut self, rhs: SI) -> Self {
self.meter += rhs.meter;
self.second += rhs.second;
self.kelvin += rhs.kelvin;
// ..
self
}
}
impl Div for SI {
type Output = SI;
const fn div(mut self, rhs: SI) -> Self {
self.meter -= rhs.meter;
self.second -= rhs.second;
self.kelvin -= rhs.kelvin;
// ..
self
}
}
// we hijack `^` operator here, but it will make code significantly nicer
impl BitXor<RHS=i8> for SI {
type Output = SI;
const fn bit_xor(mut self, rhs: i8) -> Self {
self.meter *= rhs;
self.second *= rhs;
self.kelvin *= rhs;
// ..
self
}
}
impl UnitSystem for SI {}
// f32 module
type SIVal<const UNIT: SI> = UnitValue<f32, SI, UNIT>;
const Meter = SI { meter: 1, second: 0, kelvin: 0, .. };
const Second = SI { meter: 0, second: 1, kelvin: 0, .. };
const Hertz = Second^-1;
User code:
fn foo<const U1: SI, const U2: SI>(
a: SIVal<U1>, b: SIValF32<U1>, c: SIVal<U2>
) -> SIVal<U1/U2> {
(a + b)/c
}
fn bar(length: SIVal<Meter>, time: SIVal<Second>) -> SIVal<Meter/Second^2> {
2.0*length/(time*time)
}
The drawbacks of this system are:
- Reliance on some features without implementation plan (const trait fns, const trait bounds). It can be circumvented a bit, if we'll move div and mul methods to
UnitSystem
trait, but it will make generic code a bit more verbose, as you will not be able to use*
and/
. - Unit system can not be extended by third-party code. Though arguably in practice it shouldn't be a big problem.
- You will not be able to create derivative unit systems easily, e.g. by replacing meter with millimeter.
- Generic functions will be still quite unwieldy.