As far as I can tell, the only systems that have a real answer to the commutativity problem (and similar) are term-rewriting systems using e-graphs: declare that x + y = y + x, and then try both expansions, picking whichever gives the ultimate result you like better.
Rust could support “intelligent” commutative operations (to the extent of even distinguishing between allowed and disallowed addition/multiplication of matrices) with its recent improvements to const generics if it let you define a commutative trait like the ones in the article (that don’t compile) and then you used const math generic constraints to define the requirements on M and N as it’s purely based off the shape of a matrix that math is allowed.
That is not a general solution. What do you do if x+y and y+x are both valid?
Are they the same type or different types? If they’re the same type, then the operation will be forwarded to the impl with the lhs/rhs matching how the dev typed them. If they’re different types, you would have two separate impls (x lhs + y rhs, y lhs + x lhs) and the one matching how the dev typed it would be called. Unless I’m missing something.
bytesize which I’ve been using in systemstat for a long time does something slightly interesting regarding various types – number + ByteSize impls are explicit for each numeric type via macros, but ByteSize + number is just one impl<T> Add<T> for ByteSize where T: Into<u64>.
number + ByteSize
ByteSize + number
impl<T> Add<T> for ByteSize where T: Into<u64>
I mention in the article that I was able to do something similar where LHS is a Size, one generic impl<T> Mul<T> for Size where T: IntoIntermediate covers all the primitive integers but you need the separate impls for each primitive type as the RHS.
impl<T> Mul<T> for Size where T: IntoIntermediate
I don’t use Into<u64> because a) PrettySize supports negative sizes (e.g. the difference between two sizes) so the “base” unit is i64, b) rust doesn’t provide impls for Into<uXX> from signed iXX values, c) I also support floating-point sources (e.g. Size::from_mib(1.1)) - all of which just means I have my own (sealed/private) trait called AsIntermediate that I implement via a macro for all the primitive signed, unsigned, and float types (except x128) which does a saturating conversion (e.g. u64::MAX becomes i64::MAX).
I guess again unlike bytesize, I also have a second impl even for the LHS of Size case to support ops on a reference - you need for Size and for &Size separately (again because of rust’s orphan rule) since you can’t just do impl ... for Borrow<Size> to cover both Size and &Size (this is discussed briefly in the article).
impl ... for Borrow<Size>