This proposal originates from “time units” PR.
Motivation
To make creation of values with associated unit of measure more natural and ergonomic. Currently we have to write:
let dt1: Duration = Duration::new(1, 200*1_000_000);
let dt2: Duration = Duration::from_micros(10);
let dt3: Duration = Duration::from_micros(10) + Duration::from_nanos(200);
// Units are from simple_units crate
let distance1: Meter = Meter(1.0);
let distance2: Meter = Meter(5e-9);
let accel1: MeterPerSecond2 = MeterPerSecond2(2.5*G); // here G = 9.80665
let accel2: MeterPerSecond2 = MeterPerSecond2(3.0);
This proposal instead will allow us to write:
use std::time::duration_literals;
let dt1: Duration = 1s + 200ms; // or just 1.2s, see further
let dt2: Duration = 10us;
let dt3: Duration = 10.2µs; // µs and us are equivalent, proposal can support both
use simple_units::literals::*;
let distance1: Meter = 1m;
let distance2: Meter = 5nm;
let accel1: MeterPerSecond2 = 2.5g;
let accel2: MeterPerSecond2 = 3[m/s^2]; // see further on how square brackets work
This feature should make Rust a bit more suitable for writing scientific and engineering code, as well as more approachable for target audience from those fields.
Design
To define custom suffixes for integer and float literals crate will have to define the following functions decorated with #[float_literal(..)]
or #[int_literal(..)]
:
pub struct Meter(pub f64);
// unfortunately we can't make it `const fn` right now
// more variants can be supported, e.g. `meter`, `meters`, `feets`, etc.
#[float_literal("au", "km", "m", "cm", "mm", "um", "µm", "nm")]
pub fn float_meter_literals(n: f64, sfx: &str) -> Meter {
Meter(match sfx {
"au" => METERS_PER_AU*n,
"km" => 1e3*n,
"m" => n,
"cm" => 1e-2*n,
"mm" => 1e-3*n,
"um" | "µm" => 1e-6*n, // note that "um" and "µm" are equivalent
"nm" => 1e-9*n,
_ => unreachable!(),
})
}
// this will allow us to write `1m` instead of `1.0m`
#[float_literal("au", "km", "m", "cm", "mm", "um", "µm", "nm")]
pub fn int_meter_literals(n: i64, sfx: &str) -> Meter {
float_meter_literals(n as f64, sfx)
}
Functions should accept two arguments:
- integer/float with a concrete type (floats for
float_literal
and integers forinteger_literal
) - suffix string
if signature is not compatible compilation error will be issued.
Now to use custom literals int_meter_literals
or/and float_meter_literals
should be in the current scope. Functions which define suffixes shouldn’t conflict with each other, e.g. if we use m
for both minutes and meters, function which handle conversion should not be in the same scope.
When compiler encounters non-builtin suffix (i.e. u8
, u16
, f32
, etc.), for example 1m
it searches functions with #[integer_literal(..)]
attribute. If it can’t find the specified suffix, “unknown suffix” compilation error will be issued. If function was found 1m
gets desugared into int_meter_literals(1, "m")
. Note that the usual overflowing literal check will be applied here as well.
Some units can’t be used as suffixes, e.g, m/s^2
. To overcome this restriction square bracket can be used to explicitly denote that custom suffix is used, i.e. 1m
and 1[m]
are equivalent to each other. 5.3[m/s^2]
in turn will be desugared into float_acceleration_literals(5.3, "m/s^2")
. Square brackets are traditional for denoting units of measure in scientific literature. (though it’s not strictly correct from the point of view of ISO 31-0) This functionality shouldn’t conflict with indexing, as it can bit applied only to {integer}
and {float}
literals.
With advancement of const fn
capabilities custom literals could be used for constants definitions.
In addition to units this approach can be also used for definition of complex numbers and quaternions.
Drawbacks and alternatives
The main drawback of the proposal is complication of the language, and potential ambiguity of custom literals, especially if suffixes from several sources will be mixed.
The main alternative is to use extension traits over primitive types without any syntactic sugar. In this approach trait(s) will be defined (e.g. SiTimeUnits
) and implemented e.g. for u32
and f32
. The example from the proposal beginning will look like this:
use std::time::SiTimeUnits;
let dt1: Duration = 1.s() + 200.ms();
let dt2: Duration = 10.us();
let dt3: Duration = 10.2.us(); // `µs` methods can be supported in future
use simple_units::literals::*;
let distance1: Meter = 1.m();
let distance2: Meter = 5.nm();
let accel1: MeterPerSecond2 = 2.5.g();
let accel2: MeterPerSecond2 = 3.m_per_s2(); // m/s^2 can not be supported
Arguably this approach is more noisy, less flexible, and more surprising for new Rusteceans. While custom suffix immediately makes it apparent that you are dealing not with primitive types, use of (trait) methods on primitive types will result in a certain amount of confusion. Considering that perceived Rust “noisiness” is already a problem for Rust adoption, proliferation of this approach in my opinion will only worsen the situation. Also if we’ll support many variants (m
, meter
, meters
) this approach will result in a bigger amount of code duplication.
Another alternative solution is to rely on constants:
use std::time::units::*;
let dt1: Duration = 1*SECOND + 200*MILLISECOND;
let dt2: Duration = 10*MICROSECOND;
let dt3: Duration = 10*MICROSECOND + 200*NANOSECOND; // `10.2` can't be supported
use simple_units::constants::*;
let distance1: Meter = METER;
let distance2: Meter = NANOMETER;
let accel1: MeterPerSecond2 = 2.5*G; // `G` here has type `MeterPerSecond2`
let accel2: MeterPerSecond2 = 3.*METER_PER_SECOND2;