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 make Prod<A, B> and Prod<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 make AB and Prod<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 }