I am thinking of starting a RFC regarding units of measure, as may be found in F#. A quick search didn’t show any previous RFC on the topic, which I found a bit surprising. Am I right that there is none? Otherwise, I’d be happy to take part in an existing conversation.
This is interesting. In F#, float<m/s/s>
is a parameterised float
– just like list
is taking a type parameter in list<int>
.
However, in Rust division is defined as a function in a trait (std::ops::Div
). Can the idea be translated into Rust, and can it be generalised to utilise the trait system?
I sometimes use newtyping to further “subtype” values but it has a usability cost, but with Rust’s affine type system it does provide some extra safety and security benefits. Specifically, it limits how values of the selected types can be constructed, combined, and how long they remain accessible.
Does the uom crate do what you want? There may be other similar crates too. The question for an RFC is whether this needs to be part of the standard library, which I doubt.
I’m hoping const generics becomes powerful enough that we don’t need this in the language, and we can just make a library like www.boost.org/libs/units that allows arbitrary rational powers naturally.
@cuviper uom is a nice library, but doesn’t even get close to the power of what F#'s type system can achieve.
@scottmcm Is there a RFC for const generics that you believe would make it powerful enough to represent it? I agree that the only necessary components are static rationals (which we can probably have) and a form of ad-hoc type normalization to automatically determine e.g. that km*h
is the same thing as h*km
and that either can be divided by km
to obtain h
.
Oh, and the need for a base dimension in boost is very much non-compositional, so rather problematic.
Have you seen paholg’s dimensioned crate? It can already do quite a lot of what F# can offer, working around the lack of const generics by implementing their own type level integers.
Unless I’m mistaken, dimensioned won’t let you define custom units that interact properly with existing units, e.g. seconds per lap, dollars per gallon, etc.
Also, what kind of error messages do you get with dimensioned? Unless I’m mistaken, they are going to be very, very messy.
Reading the paper, it seems F#'s units-of-measure has a key feature that a mere crate like dimensioned
could never provide: unification.
You will never be able to convince rustc that the product of Units<f64, A>
and Units<f64, B>
has the same type as the product of Units<f64, B>
and Units<f64, A>
.
F# handling of units is nice, but I seem to remember it has some problems. If you want to design a language feature this small language could give you some ideas:
Can't you just write two impls: impl Mul<Units<f64, B>> for Units<f64, A> { .. }
and impl Mul<Units<f64, A>> for Units<f64, B> { .. }
with the same associated Output
type?
FWIW the rink
crate claims to be similar to frink.
(But I’m not familiar with either.)
The Rust ecosystem is getting bigger than expected :-]
Yeah, this is the fundamental problem of supporting units of measure in type systems.
I think it might be kind of possible (but I haven't tried it) to do it in either current Rust or current Rust + specialization by giving each quantity an unique typenum number, and then choosing the product order so that the one with the minimal number comes first.
Obviously this doesn't extend very well to third-party crates since those numbers must be unique, although one could perhaps encode the crate name as a type and use it as part of the comparison.
Can’t you just write two impls
No way, no how; sorry for the confusion, but A and B were meant to be type parameters.
This isn't what I mean. dimensioned
already does this. For any two monomorphic units A
and B
, Units<f64, A> * Units<f64, B>
and Units<f64, B> * Units<f64, A>
already are the same type. (unit systems in dimensioned
use type-level fixed-size arrays of integers; they are not extensible, so your other concerns are avoided)
But if you have a generic function with two type parameters A
and B
, then no matter what you do or how many where bounds you have, these two products will never be 100% interchangeable.
...or so I was going to say. When trying to construct an example, I rediscovered just how finicky unification in rust can feel. Much to my surprise, the compiler accepted the following function and all reasonable variations of it (swapping the order of various Prods or multiplications, as long as the bounds still suggest that Prod<A, B> = Prod<B, A>
).
use std::ops::Mul;
type Prod<A, B> = <A as Mul<B>>::Output;
fn foo<A, B>(a: A, b: B) -> Prod<A, Prod<B, A>>
where
B: Copy + Mul<A>,
A: Copy + Mul<B, Output=Prod<B, A>>,
A: Mul<Prod<A, B>>,
{ a * (b * a) }
That said:
-
Having to use
where
bounds to makeProd<A, B>
andProd<B, A>
interchangeable affects your public signature, and in turn, all generic code that uses your function. If you need to rely on some other property of abelian groups in an update to the function then you are S.O.L with respect to backwards compatibility. -
Just to show how crazy unification in rust is, here's one that does fail. Apparently, the trait bound
A: Mul<B, Output = AB>
does not makeAB
andProd<A, B>
interchangeable.
use std::ops::Mul;
type Prod<A, B> = <A as Mul<B>>::Output;
// error[E0277]: the trait bound `A: std::ops::Mul<AB>` is not satisfied
fn foo<A, B, AB>(a: A, b: B) -> Prod<Prod<A, Prod<B, A>>, B>
where
B: Copy + Mul<A, Output = AB>,
A: Copy + Mul<B, Output = AB>,
A: Mul<Prod<A, B>>,
Prod<A, AB>: Mul<B>,
{ a * (b * a) * b }
Your last example can be rewritten as:
fn foo<A, B>(a: A, b: B) -> Prod<Prod<A, Prod<B, A>>, B>
where
B: Copy + Mul<A>,
A: Copy + Mul<B>,
A: Mul<Prod<B, A>>,
Prod<A, Prod<B, A>>: Mul<B>,
{ a * (b * a) * b }
But, yeah, its quite restricting and unergonomic to say at least. Also it will not allow to write the following function body with potential reordering of operations:
{
match value {
A => a1 * (b1 * a2) * b2,
B => (a1 * b1) * a2 * b2,
C => (a1 * a2 * a3 / a4) * b1 * b2,
}
}
@leonardo If I read frink’s docs correctly, its units of measure system is just as limited as dimensioned or uom.
Also, could you elaborate on the problems with F#'s units of measure system?
And yes, the F# type system has a nice equivalence relation/normalization among units of measure, and unification for units of measure type variables, which cannot be implemented in today’s Rust as far as I can tell.
Boost.Units (and the Units ISO C++ proposal) is pretty nice and to implement it the only thing one needs AFAICT is specialization. How is this better ?
While I haven’t stared too hard at Boost.Units, I rather doubt that any solution that is sufficient for C++ is going to be sufficient for Rust, because of the fundemental differences between C++ templates and rust generics. Unification is not a problem in C++.