Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wellcaffeinated opened this issue Jun 12, 2019 · 11 comments

Comments

@wellcaffeinated
Copy link

(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.

@wellcaffeinated wellcaffeinated changed the title Question: accepting generic dimension and for calculation in specific units Question: accepting generic dimension for calculation in specific units Jun 12, 2019
@paholg
Copy link
Owner

paholg commented Jun 13, 2019

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
Copy link
Author

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
Copy link

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

@wellcaffeinated
Copy link
Author

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
Copy link
Contributor

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
Copy link
Owner

paholg commented Jun 13, 2019

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
Copy link
Author

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
Copy link
Owner

paholg commented Jun 13, 2019

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
Copy link
Author

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

@paholg
Copy link
Owner

paholg commented Jun 13, 2019

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
Copy link
Author

ok thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants