What is the difference between `trait NumOps<Rhs = Self, Output = Self>: Add<Rhs, Output = Output>{}` and `trait NumOps: Add<Self, Output = Self>{}`?

I've been looking at the num-traits crate and how to define abstractions for numbers.
num-traits defines the following Trait to abstract over mathematical operations:

pub trait NumOps<Rhs = Self, Output = Self>:
    Add<Rhs, Output = Output>
    + Sub<Rhs, Output = Output>
    + Mul<Rhs, Output = Output>
    + Div<Rhs, Output = Output>
    + Rem<Rhs, Output = Output>
{
}

Which looks fine, just a bit more complicated than I naively would have assumed it to look.
A simplified (and reduced to addition) definition seems to do the same thing, but actually will not compile:

pub trait NumOps: Add<Self, Output = Self>{}

The compiler will complain with the following error:

   |
5  | trait Number: Add<Self, Output = Self> {}
   |               ^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
   |
note: required by a bound in `std::ops::Add`
[...]
   |
76 | pub trait Add<Rhs = Self> {
   |               ^^^^^^^^^^ required by this bound in `Add`
help: consider further restricting `Self`
   |
5  | trait Number: Add<Self, Output = Self> + Sized {}
   |                                        +++++++

Why is there this additional requirement? It seems the first version just defines a placeholder for the Rhs and Output, but eventually both are filled with Self. Is there some fundamental difference in the syntax used to define the two traits?

That's because generic parameters have an implicit Sized bound, while Self has not.

So to simplify event more, this:

trait MyAdd<Rhs>{}
trait MyNumber: MyAdd<Self>{}

is equivalent to this:

trait MyAdd<Rhs: Sized>{}
trait MyNumber: MyAdd<Self>{}

and it does not compile, because Self is allowed to be !Sized while Rhs is not. You can fix it by restricting Self as in:

trait MyAdd<Rhs>{}
trait MyNumber: MyAdd<Self> + Sized{}

... or relaxing Rhs (well... you can only do this if Add is under your control, of course)

trait MyAdd<Rhs: ?Sized>{}
trait MyNumber: MyAdd<Self>{}

The implicit bound exists just for convenience.

In std::Add it exists (i.e. is not relaxed) because the Add::add method takes the rhs parameter by value, so it must be Sized.

2 Likes

Also, in your initial example, this:

pub trait NumOps<Rhs = Self, Output = Self>:
    Add<Rhs, Output = Output> {}

is equivalent to this:

pub trait NumOps<Rhs: Sized = Self, Output = Self>:
    Add<Rhs, Output = Output> {}

... so everything works out.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.