I am trying to encapsulate numbers in units of measurements, and also to support explicit unit conversions. I have written the following code:
use std::marker::PhantomData;
trait MeasurementUnit {
const RATIO: f64;
}
#[derive(Debug, PartialEq, Clone, Copy)]
struct Measure<Unit> {
value: f64,
phantom: std::marker::PhantomData<Unit>,
}
impl<Unit: MeasurementUnit> Measure<Unit> {
fn convert<DestUnit: MeasurementUnit>(&self) -> Measure<DestUnit> {
Measure::<DestUnit> {
value: self.value * (Unit::RATIO / DestUnit::RATIO),
phantom: PhantomData,
}
}
}
struct Inch;
impl MeasurementUnit for Inch {
const RATIO: f64 = 1.;
}
struct Foot;
impl MeasurementUnit for Foot {
const RATIO: f64 = 12.;
}
struct Hour;
impl MeasurementUnit for Hour {
const RATIO: f64 = 3600.;
}
fn main() {
let m1 = Measure::<Foot> { value: 7.2, phantom: PhantomData };
let m2: Measure::<Inch> = m1.convert::<Inch>();
println!("{} feet are {} inches.", m1.value, m2.value);
let m3: Measure::<Hour> = m1.convert::<Hour>();
println!("{} feet are {} hours.", m1.value, m3.value);
}
Using this code, I can convert feet to inches, but also feet to hours! Of course that does not make sense (except in advanced physics), as feet are units of length, while hours are units of time.
I would like to have that every conversion between two units of different quantities generate a compilation error, without adding any run-time cost.
I could do that in C++, but I cannot in Rust.
I tried to write the following code, that is not valid, though. The added lines are marked with "//Added":
use std::marker::PhantomData;
fn assert_same_type<T>(_: T, _: T) {} //Added
trait MeasurementUnit {
type Quantity; //Added
const RATIO: f64;
}
#[derive(Debug, PartialEq, Clone, Copy)]
struct Measure<Unit> {
value: f64,
phantom: std::marker::PhantomData<Unit>,
}
impl<Unit: MeasurementUnit> Measure<Unit> {
fn convert<DestUnit: MeasurementUnit>(&self) -> Measure<DestUnit> {
//assert_same_type(Unit::Quantity, DestUnit::Quantity); //Added
Measure::<DestUnit> {
value: self.value * (Unit::RATIO / DestUnit::RATIO),
phantom: PhantomData,
}
}
}
struct Length; //Added
struct Time; //Added
struct Inch;
impl MeasurementUnit for Inch {
type Quantity = Length; //Added
const RATIO: f64 = 1.;
}
struct Foot;
impl MeasurementUnit for Foot {
type Quantity = Length; //Added
const RATIO: f64 = 12.;
}
struct Hour;
impl MeasurementUnit for Hour {
type Quantity = Time; //Added
const RATIO: f64 = 3600.;
}
fn main() {
let m1 = Measure::<Foot> { value: 7.2, phantom: PhantomData };
let m2: Measure::<Inch> = m1.convert::<Inch>();
println!("{} feet are {} inches.", m1.value, m2.value);
let m3: Measure::<Hour> = m1.convert::<Hour>();
println!("{} feet are {} hours.", m1.value, m3.value);
}
The assert_same_type call fails to compile if its arguments have a different type, and does nothing if they have the same type. The problem is that I cannot instantiate an associated type.
Any suggestions?