-
Notifications
You must be signed in to change notification settings - Fork 2
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
Unit ergonomics #56
Comments
I wrote down some thoughts about how I'd like to modify the current syntax / API. Unit Syntax
So after playing with this a little more I found out that there is a different aproach in which this actually works if implemented correctly. Will require a bit of magic to set it up properly, since the system needs to be able to track unit magnitudes at compile time (without So what I am thinking of is taking this opportunity to really think about what kind of syntax/organization structure we want, because I'd prefer to get it right now and not to have to change it (too much) in the future, because that's a pretty breaking change. In my playground crate, I currently implemented it so that the following example compiles: use crate::units::{Meter, Second, s, m, Kilometer, Hour, Mile};
pub fn main() {
let vel1 = 1.0 * m / s;
let vel2 = (Meter / Second)::new(2.0);
println!("{} m/s", vel1.value_in(Kilometer / Hour));
println!("Rounded: {} mph", vel2.round_in(Mile / Hour));
} I personally find this syntax really clear and nice to read (it's a shame it's The unit math is all evaluated at compile time, so no actual multiplication should be taking place at run time for the On a side note, I think this will also lay the groundwork for heavier compile-time analysis. For example, I think we might be able to analyze whether certain unit conversions are safe, but I haven't checked this. Symbols: storage-type specific or not?In the above code, symbols and units mean exactly the same thing. use crate::units::{Meter, Second, Kilometer, Hour, Mile};
use crate::units::f32::{s, m};
pub fn main() {
let vel1 = 1.0 * m / s;
let vel2 = (Meter / Second)::new(2.0);
println!("{} m/s", vel1.value_in(Kilometer / Hour));
println!("Rounded: {} mph", vel2.round_in(Mile / Hour));
} while I'd keep the units for the storage type agnostic methods such as I am not sure if this simplifies or complicates things for the user. The alternative I see would be to consider symbols and units the same thing and simply require a type annotation here and there. Dimensions: storage-type specific or not?At the same time I am also reconsidering what the best way to define the dimensions is. The way I see it there are three options:
Personally, I'd prefer option three and simply giving the user the choice, but I can also see an argument against it - there are suddenly many different types called Module structureAs a last thing, I'd like to think about the structure of the macro output from use diman::si::dimensions::{Velocity, Time};
use diman::si::dimensions::f32::{Length};
use diman::si::units::{Meter, Second};
use diman::si::symbols::f32::{m, s}; // Provided we'd implement it this way
use diman::si::constants::PROTON_MASS; I personally prefer this and find it cleaner, but I guess it might be annoyingly busy for some people. |
Neat ideas, and I'd be curious to play with an experimental branch. My subjective thoughts: Unit syntaxI like mod gauge {
use diman::si::units::{Meter, Second};
pub type Velocity = Meter / Second;
}
// then
let v = gauge::Velocity::new(2.0); I'm a bit less enthusiastic about the Storage-type specific?I don't feel any pain from In the above, how can you make Module structureIt looks sort of tedious/a lot to remember as written and rust-analyzer won't know which of the several submodules to get |
Thanks for your input!
I should get one up soon, am currently in the process of integrating this new implementation into the macro, but it'll be a bit of work.
Note that there would still be
That's true. The
So I suppose that if the user gets the dimension wrong in a struct, they'd still see something like this to hopefully help them out. mod gauge {
use diman::si::units::{Meter, Second};
pub type Velocity = Meter / Second;
}
// then
let v = gauge::Velocity::new(2.0); unfortunately cannot work at the moment. The dark secret that I haven't mentioned yet is that I'll write down why here, mainly so I can read it when I inevitably come back to this in a few months, wondering why I didn't implement it differently:
All of this said, I am not incredibly happy about the units being "constants". I think that the main source of confusion regarding this might be if somebody mistakes
That's a fair point. I'm a little worried about namespace conflicts here, since already
I don't think you're alone in it either. I actually regret using fully qualified aliases - initially I wrote diman for use in a code where I could only use I think for now, I'll change it to having
I think it should be possible to implement it exactly like the units, except this time it is a properly named constant. The value of the constant would be stored in the
The first I agree with. If only storage-type generic dimensions exists, then it also help with rust-analyzers auto import, because One last comment - since floats are annoying and |
Thanks for writing this out. There has been some recent Zulip discussion on const float generics. I think clear use cases would be helpful, but the main hangup looks to be signed zero and NaN. The |
Thanks for the link, I added a comment for our use case there. |
In the future I'd like if instead of generating the constructors
Length::meters
,Time::seconds
, ... as well as the conversion functionsin_meters
,in_seconds
,in_joules
for every unit that exists, we'd have a syntax (loosely inspired by https://github.com/aurora-opensource/au and https://github.com/mpusz/mp-units)that reads something like
I think there are a number of benefits to a syntax like this:
in
function would only have to be generated once and is then monomorphized for with which it is used, which I (blindly, I should say) assume should be faster than generating the methods whether the unit is used or not. This might not be true ifin
is the only method that exists, but I assume there will be more methods like it in the future.let vel_float: f64 = vel.in(Kilometer / Hour)
which would be very nice. However, that is pretty futuristic at this point, since this requires - at the very least -const_trait_impl
(Tracking issue for RFC 2632,impl const Trait for Ty
and~const
(tilde const) syntax rust-lang/rust#67792) but in order to do this without any overhead, it might also require even more machinery that is futuristic at this point.In order to implement this, I'd add ZSTs for each unit (i.e.
struct Meter
) which then implement a unit trait likeThe
in
method would then be implemented (roughly, the real implementation isnt quite as simple, since we need to ensure the proper dimensions) asI played around with this and found that a few things about this are not possible exactly like I want them at the moment.
unboxed_closures
&fn_traits
feature) rust-lang/rust#29625), so it would have to be more likelet l = Meters::new(10.0);
or alternativelylet l = meters(10.0)
wheremeters
is a function we'd need to generate.in
since thats a reserved keyword. Sounds trivial, but I do think that unfortunately severely reduces readability, since the alternative would be something likelet in_km = l.in_unit(Kilometer);
which is not as nice and doesn't really improve onl.in_kilometers()
which is the current option.Because of these concerns, I don't know that this would be an improvement over the current syntax, as long as the constructors and
in_...
are the only conversion / unit-specific methods on Quantity.The text was updated successfully, but these errors were encountered: