paholg / dimensioned

Compile-time dimensional analysis for various unit systems using Rust's type system.
https://crates.io/crates/dimensioned
MIT License
300 stars 23 forks source link

Question: accepting generic dimension for calculation in specific units #64

Open wellcaffeinated opened 5 years ago

wellcaffeinated commented 5 years ago

(I'm sorry. I'm very new to rust and having a hard time understanding the type system.)

What I'm trying to achieve is a method on a struct that accepts any temperature, and uses it in a calculation to return a unitless value.

I'm trying to do something like this:

impl TemperatureDependence for MyStruct {
  fn apply<T>(&self, n :Vector3<f64>, temperature :T) -> Vector<f64> 
  where T :Temperature {
    // --snip--
    // this should be unitless.. not sure if I should use dimensioned::si::Unitless... 
    let f :f64 = (temperature - 20 * si::K) / si::K;
    // --snip--
    // returns a vector based on this
  }
}

But I get the error:

binary operation - cannot be applied to type T note: T might need a bound for std::ops::Sub

I feel like i'm missing something, because i don't actually want to make this THAT generic. I feel like any type that implements trait Temperature should be able to be converted into Kelvin... so what gives? How do i do this?

Thanks in advance.

paholg commented 5 years ago

The easiest way to do this is to accept something that can convert into Kelvin.

You will also need to call value() (part of trait Dimensionless) to get the underlying f64 value once you have a unitless quantity:

Example:

use dimensioned::{si, Dimensionless};

impl TemperatureDependence for MyStruct {
  fn apply<T>(&self, n: Vector3<f64>, temperature: T) -> Vector<f64> 
  where T: Into<si::Kelvin<f64>> {
    // --snip--
    let f: f64 = *((temperature.into() - 20.0 * si::K) / si::K).value();
    // --snip--
    // returns a vector based on this
  }
}

If you really want to accept anything that is a Temperature without conversion, to which you'll add a constant, you'll probably end up having to do some hacks with the One trait in num or something like it, and it is likely to be a lot more work than it's worth.

wellcaffeinated commented 5 years ago

That's fantastic! Thanks. Everything makes sense to me except for dereferencing that computation.

Presumably those brackets return a reference... but i'm not sure why. And doesn't calling a method on a reference automatically dereference it?

paho-outreach commented 5 years ago

value() returns a reference. It should probably return a value, but it's too late to change without breaking changes.

wellcaffeinated commented 5 years ago

Ohhh I see. I got the order of operations mixed up.

But now that I think about it, if I implement the function with this trait requirement and try to write another function that uses this one, won't that requirement ascend up the chain? Do I have to constantly worry about T: Into<si::Kelvin<f64>>?

eg:

fn otherFn<Q, T>( &tempDep: Q, n: Vector3<f64>, temp: T) -> Vector3<f64>
where 
  Q: TemperatureDependence,
  T: Into<si::Kelvin<f64>> // do I need to worry about this every time I use a temperature now?
{
   tempPep.apply(n, temp)
}
adeschamps commented 5 years ago

value() returns a reference. It should probably return a value, but it's too late to change without breaking changes.

For what it's worth, I would be happy to see see this change, and update my own code to deal with it. Overall, it would simplify things.

paholg commented 5 years ago

But now that I think about it, if I implement the function with this trait requirement and try to write another function that uses this one, won't that requirement ascend up the chain? Do I have to constantly worry about T: Into<si::Kelvin<f64>>?

Yeah, that's how generics in Rust work. Alternatively, those functions "up the chain" could accept any concrete type that implements that trait, such as taking si::Kelvin<f64> directly.

For what it's worth, I would be happy to see see this change, and update my own code to deal with it. Overall, it would simplify things.

I have a few breaking changes in mind that I'd like to save until I can replace typenum with const generics. This is one of them.

wellcaffeinated commented 5 years ago

Oh that's too bad. I was hoping this library would implement something more along the lines of this: https://ferrisellis.com/content/rust-implementing-units-for-types/

This strategy allows for accepting a Length in any units without declaring the input units.

fn circumference<T>(r: Length<T>) -> Length<T> where T: LengthUnit {
    2 * r * std::f64::consts::PI
}
fn main() {
    let l1 = millimeters!(10);
    let l2 = meters!(5);
    let l3 = (5 * l1) + l2;
    let l3_meters = f64::from(meters!(l3));
    let c1 = circumference(l1);

    println!("l1 = {}", l1);
    println!("l2 = {}", l2);
    println!("l3 = (5 * l1) + l2 = {}", l3);
    println!("l3_meters = {}", l3_meters);
    println!("circumference(radius = {}) = {}", l1, c1);
    println!("l3 > l2 : {}", l3 > l2);
    println!("l3 / l2 = {}", l3 / l2);
}
paholg commented 5 years ago

This strategy allows for accepting a Length in any units without declaring the input units.

It allows the appearance of accepting a Length in any units. In reality, lengths are only represented as nanometers. If you need to operate with greater than nanometer precision, or at astronomical distances, that type is useless.

Dimenionsed takes an alternate approach, letting you represent values in any units you wish, with the precision you wish, with the trade-off of additional boilerplate for generic functions.

You could achieve something similar in Dimensioned by, for example, just using si, and converting on output.

wellcaffeinated commented 5 years ago

Ok i guess I'm trying to write my code to generally and I should just be using Kelvin

paholg commented 5 years ago

Here's what that example might look like in dimensioned:

use dimensioned::{si, f64prefixes::*};

fn circumference(r: si::Meter<f64>) -> si::Meter<f64> {
    2.0 * r * std::f64::consts::PI
}

fn main() {
    let l1 = 10.0 * MILLI * si::M;
    let l2 = 5.0 * si::M;
    let l3 = (5.0 * l1) + l2;
    let l3_meters = l3.value_unsafe;
    let c1 = circumference(l1);

    println!("l1 = {}", l1);
    println!("l2 = {}", l2);
    println!("l3 = (5 * l1) + l2 = {}", l3);
    println!("l3_meters = {}", l3_meters);
    println!("circumference(radius = {}) = {}", l1, c1);
    println!("l3 > l2 : {}", l3 > l2);
    println!("l3 / l2 = {}", l3 / l2);
}

The difference here is that all prints will be in meters, it would be up to you to output in a different unit if desired.

wellcaffeinated commented 5 years ago

ok thanks!