Motivation
One of the key paper cut encountered when implementing binary operators over custom types is that the dispatch is driven by the left-hand operand, and the ability to add binary operators on the left-hand operand is limited at the moment by E0210.
Consider the following Playground link:
#[derive(Clone, Copy, Debug)]
pub struct Speed(f64);
impl MulAssign<f64> for Speed {
fn mul_assign(&mut self, other: f64) { self.0 *= other; }
}
impl Mul<f64> for Speed {
type Output = Self;
fn mul(mut self, other: f64) -> Self { self *= other; self }
}
impl Mul<Speed> for f64 {
type Output = Speed;
fn mul(self, mut other: Speed) -> Speed { other *= self; other }
}
The latter implementation is allowed because f64
is a concrete type.
On the other hand:
#[derive(Clone, Copy, Debug)]
pub struct Tagged<V, T> {
value: V,
tag: PhantomData<fn(T) -> T>,
}
impl<V, T> MulAssign<V> for Tagged<V, T>
where
V: MulAssign,
{
fn mul_assign(&mut self, other: V) { self.value *= other; }
}
impl<V, T> Mul<V> for Tagged<V, T>
where
V: MulAssign,
{
type Output = Self;
fn mul(mut self, other: V) -> Self { self *= other; self }
}
impl<V, T> Mul<Tagged<V, T>> for V
where
V: MulAssign,
{
type Output = Tagged<V, T>;
fn mul(self, mut other: Tagged<V, T>) -> Tagged<V, T> { other *= self; other }
}
On the other hand, the latter implementation fails due to E0210, as V
is not "covered".
This means that code like untagged * tagged
will later fails to compile, as there'll be no implementation of Mul
which matches.
Guide-level Explanation
In Rust, the binary operators can be dispatched either by left-hand side, or right-hand side operand:
Add
,Sub
,Mul
,Div
,PartialEq
,PartialOrd
, etc... are dispatched by left-hand side.AddAlt
,SubAlt
,MulAlt
,DivAlt
,PartialEqAlt
,PartialOrdAlt
, etc... are dispatched by right-hand side.
Dispatching by right-hand side is useful for implementing operators on generic left-hand side values:
#[derive(Clone, Copy, Debug)]
pub struct Tagged<V, T> {
value: V,
tag: PhantomData<fn(T) -> T>,
}
// error[E0210]: type parameter `V` must be covered by another type when it appears
// before the first local type (`Tagged<_, _>`)
impl<V, T> Mul<Tagged<V, T>> for V
where
V: MulAssign,
{
type Output = Tagged<V, T>;
fn mul(self, mut other: Tagged<V, T>) -> Tagged<V, T> { other *= self; other }
}
impl<V, T> MulAlt<V> for Tagged<V, T>
where
V: MulAssign,
{
type Output = Self;
fn mul_alt(mut self, other: V) -> Self { self.value *= other; self }
}
In the case of two implementations such Add
and AddAlt
both being available, depending on whether dispatching by left-hand side operand or right-hand side operand, then the implementation dispatching by left-hand side operand is used.
Reference-level Explanation
For each binary operator dispatching by left-hand side, a binary operator dispatching by right-hand side is introduced, with the suffix Alt
.
For example:
trait Mul<Rhs = Self> {
type Output;
fn mul(self, other: Rhs) -> Self::Output;
}
trait MulAlt<Lhs> {
type Output;
fn mul_alt(self, other: Lhs) -> Self::Output;
}
When resolving a binary operation such as x * y
:
- First, an implementation of
Mul<Y> for X
is looked up. - If this lookup succeeds, then this implementation is used.
- Otherwise, an implentation of
Mul<X> for Y
is looked up. - If this lookup succeeds, then this implementation is used. Do mind that it reverses arguments.
- Otherwise, an error is emitted.
Drawbacks
This may unreasonably (I have 0 clue) affect the resolution of binary operations. In particular, the extra flexibility in operator choice may make type inference a lot more difficult in complex scenarios. It is my hope that this drawback is strongly mitigated by the strict precedence offered to existing operators... but who knows.
A lesser drawback is the increase in API surface, and the extra documentation effort & potential for confusion for users which may result.
Rational and alternatives
There does not exist any reasonable alternative, language-wise, as far as I can see. E0210 exists for a reason.
Notably, whilst most binary operators are commutative (for integrals/floats) Sub
and Div
stand out as being non-commutative, and therefore rule out the idea of translating x <bin> y
to Y::bin(y, x)
.
Prior art
None?
Would Haskell extremely flexible operator definitions fit here?
Unresolved questions
It is unclear whether this would make type-inference undecidable, or simply too costly for large arithmetic expressions.
Future possibilities
None. Once done, overloading operators is always possible.