Pre-RFC: implementing the Add-trait from the RHS

Summary

This RFC proposes the inclusion of RAdd, RSub, … and RAddable, RSubtractable, … traits in the core library, that allow for the implementation of the Add trait from the RHS.

Motivation

There is currently no way to implement the Add/Sub/… trait for a type outside the module, leaving libraries that work with those traits with either ugly wrapper types or with just one side that can be any type, which is especially troubling for asymmetric operations like Sub or Div.

Detailed Design

There would be two new traits per operation, one that is used by the end user, and one that is used to allow said user to implement the traits from the other side. Example for Add:

trait RAdd<T> {
    type Output;
    
    fn r_add(self, other: T) -> Self::Output;
}

trait RAddable {}

impl<T, U> Add<U> for T where T: RAddable, U: RAdd<T> {
    type Output = U::Output;

    fn add(self, other: U) -> U::Output {
        other.r_add(self)
    }
}

RAddable would be implemented for u8 - u128, i8 - i128, f32 and f64. This allows library writers to implement Add for those types, without breaking any existing code.

Drawbacks

  • only useful for libraries introducing some new types that interact with primitives, but would be around forever once stabilized

Alternatives

  • Using methods on your own type (subtracted_from, …)
  • Implement the operator traits for primitives instead of using the RAddable, … traits

Unresolved questions

  • Should types like Vec implement the RAddable, RSubtractable, … traits?
  • Should types like Option or Result implement RAddable, …? Maybe only if the encased type implements those traits?
  • Should operations like Index be supported?
  • Should the RAddable, RSubtractable, … traits be replaced with pnly one trait?

It’s possible to implement external traits like Add for external types, as long as it is parameterized by a local type. For example, this program compiles:

struct Foo;

impl std::ops::Add<Foo> for i32 {
    type Output = Foo;
    fn add(self, x: Foo) -> Foo { Foo }
}

fn main() {
    let x = 3 + Foo;
}

As motivation for this proposal, it would be useful to show an example that isn’t possible in Rust today. (Edit: For example, a generic impl bounded by a local trait may not work.)

2 Likes

I don’t believe this would actually solve any of the coherence problems that the current restriction is intended to avoid. You can’t have this blanket impl and also the impls of Add you are allowed to write today.

It isn’t always possible:

struct Foo;

impl<T> std::ops::Add<Foo> for T {
    type Output = Foo;

    fn add(self, other: Foo) -> Foo { Foo }
}

doesn’t compile. And although this (minimal) example wouldn’t really make sense in the real world, this one would:

use std::ops::Add;

struct Complex<T, U>(T, U);

trait Real {}

// works:
impl<T, U, V> Add<V> for Complex<T, U> where T: Add<V>, V: Real {
    type Output = Complex<<T as Add<V>>::Output, U>;

    fn add(self, other: V) -> Complex<<T as Add<V>>::Output, U> {
        Complex(self.0 + other, self.1)
    }
}

// doesn't work:
impl<T, U, V> Add<Complex<T, U>> for V where V: Add<T> + Real {
    type Output = Complex<<V as Add<T>>::Output, U>;

    fn add(self, other: Complex<T, U>) -> Complex<<V as Add<T>>::Output, U> {
        Complex(other + self.0, self.1)
    }
}

I’d really prefer something that doesn’t introduce multiple ways to do things, but I don’t really have any other solution to the problem (see my answer before this one), other than using ugly wrapper types.

This is a valid position, but you haven't actually resolved the issue here. This just wouldn't compile if we tried to add it to the standard library (or if it did it would break peoples' code, not certain).

A proposal that makes sense (but I don't know if I agree with it) would be to add RAdd<T> et al, no blanket impls, and then if there is both an LHS: Add<RHS>and RHS: RAdd<LHS> impl, to define that to delegate to Add and not RAdd.

That would be better, but it would either need negative where-clauses or special handling from the compiler. My proposal wouldn’t break any code, because of the RAddable, … traits, that are needed for the blanket impl. If those negative where-clauses are added, those additional traits wouldn’t be needed, and a simple

impl<T, U> Add<U> for T where T: !Add<U>, U: RAdd<T> {
    ...
}

would suffice.

Yes, it would need special handling from the compiler (we're not going to add general purpose negative where clauses because the problem of the excluded middle and our goal of making adding impls backwards compatible). Binary operators already have this though.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.