Checking associated types

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?

1 Like

Here's one way. It has the benefit of making explicit what both units of the ratio are. Every category of measurement (time, length) is implicit in the BaseUnit (Second, Inch).

The trait bounds on convert(...) takes the place of your assertion.

1 Like

Does this idea help ?

The basic idea is that type T must have precise implementation MeasurementUnit<U>, where T and U are unit of measurement.

You can do something like this:

trait CmpType {
    type Is:?Sized;
}
impl<T:?Sized> CmpType for T {
    type Is = T;
}

fn assert_type_eq<T1:?Sized, T2:CmpType<Is=T1>+?Sized>() {}
fn assert_typeof_val<T:?Sized>(_:&T) {}
fn assert_same_type<T:?Sized>(_:&T, _:&T) {}

fn main() {
    assert_type_eq::<usize, usize>();
    assert_type_eq::<str, str>();
    // assert_type_eq::<(), usize>();  // Compile error
    
    assert_typeof_val::<usize>(&4);
    //assert_typeof_val::<str>(&4);   // Compile error
    
    assert_same_type(&5,&7);
    // assert_same_type(&5u8, &7usize);   // Compile error
}

(Playground)

Thank you. I missed the trait bound Quantity = Unit::Quantity in the definition of convert. So, my solution is this:

use std::marker::PhantomData;
trait MeasurementUnit {
    type Quantity;
    const RATIO: f64;
}
struct Measure<Unit> {
    value: f64,
    phantom: std::marker::PhantomData<Unit>,
}
impl<Unit: MeasurementUnit> Measure<Unit> {
    fn convert<DestUnit: MeasurementUnit<Quantity = Unit::Quantity>>(&self) -> Measure<DestUnit> {
        Measure::<DestUnit> {
            value: self.value * (Unit::RATIO / DestUnit::RATIO),
            phantom: PhantomData,
        }
    }
}

struct Length;
struct Inch;
impl MeasurementUnit for Inch {
    type Quantity = Length;
    const RATIO: f64 = 25.4;
}
struct Foot;
impl MeasurementUnit for Foot {
    type Quantity = Length;
    const RATIO: f64 = 304.8;
}
struct Time;
struct Hour;
impl MeasurementUnit for Hour {
    type Quantity = Time;
    const RATIO: f64 = 3600.;
}
fn main() {
    let feet = Measure::<Foot> {
        value: 7.2,
        phantom: PhantomData,
    };
    feet.convert::<Inch>();
    //WRONG feet.convert::<Hour>();
}
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.