As you may guess, what I'm trying to do here is to define specific "versions" of f64 in order to prevent an angle expressed in degrees to be passed to a function that expects radians, and vice versa. But, under the hood, all the three do_something* functions are identical (at least... I think).
I'm not an expert, but I was expecting the compiler to produce the same code for these functions, or more generally to see my single value structs Radians and Degrees to be treated exactly like f64. Accordingly to Compiler Explorer, this is not the case: Compiler Explorer . do_something_with_radians and do_something_with_degrees are identical but they differ from do_something, both in the inner code and in the call site; there's a little more machine code.
For code that's not performance critical I wouldn't care, but in case of a measurable performance hit I would stay with simple f64s exposing myself to some potential bugs due to mixing of radians and degrees (in this example).
So... There's something I could do better here?
Less important question: there's a way to avoid the .0 to access the struct's inner value and do calculations on it?
Why have you expected that? These are different function, they deal with different types, one needs to pack/unpack things, one doesn't need to do that.
To request optimization when using Compiler Explorer, add -C opt-level=3 to the compiler flags. In that case, all 3 of your functions will, if you convince the compiler not to inline or delete them entirely, compile into the same symbol (because identical machine code is deduplicated). Demo:
pub fn do_something_with_degrees(Degrees(num): Degrees) -> Degrees {
Degrees(num * num + num / num)
}
Or afterward:
pub fn do_something_with_degrees(deg: Degrees) -> Degrees {
let Degrees(deg) = num;
Degrees(num * num + num / num)
}
Or you can implement the std::ops traits so that your new type supports arithmetic operators. (When doing this, you should think about which ones make sense; for example, multiplying Degrees by Degrees rarely makes sense unless it's part of some larger formula, so you might choose to omit such an implementation.)
You should also #[derive(Clone, Copy)] whether or not you do that, to make it convenient to use.
That's the way to go, if you actually want to avoid errors such as adding Degrees to Radians. The .0 should be private to the module/crate implementing the operations, and only the operations should be public.
Multiplying Degrees by Degrees may make sense, but then that doesn't return Degrees, it returns DegreesSquared!
Radians are a weird case because really, radians are unit-less. 1 Radian = 1 meter / 1 meter = 1.
Whether a quantity has units (or more precisely, dimensions โ "meter" is a unit and "length" is a dimension) is in part a matter of practicality. Just because the definition of the radian arises from a computation where all the units cancel out does not mean we can't choose to add dimensions to help us avoid making mistakes. The only cost is that some functions need conversions where they wouldn't otherwise. If you're performing calculus, yes, certainly you don't want the input of sin() to be โin radiansโ rather than dimensionless. But most programs are not doing that, and even the ones that do numeric integration may still benefit from additional dimensions/newtypes in their function signatures.
Sure, yeah, no problem with creating a radian unit and converting when necessary. Radians convert to unit-less with a factor of 1 radian = 1, whereas degrees can also be converted to unit-less with a factor of 1 degree = ฯ / 180.