Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do Rust's operators have the type Output variable? [duplicate]

I've been reading through the Rust book online and I've reached 4.1, Operators and Overloading. I noticed that std::ops::Add defines it as fn add(self, rhs: RHS) -> Self::Output; with a type Output defined separately in the trait.

I understand what's going on here: the add function accepts the left hand side as self, and the right hand side as the generic parameter type RHS.

I want to know why is the output type defined with the Output alias, instead of being another generic (ex. Add<RHS, Output>)? Is it just a convention, or is there a specific reason to it?

like image 454
MutantOctopus Avatar asked Aug 24 '16 05:08

MutantOctopus


2 Answers

While functions operate on variables, you can think of traits as functions that operate on types. When thought of this way, the type parameters serve as inputs to the function and associated types serve as outputs.

Since Output is an associated type, for two types A and B that we wish to impl Add, we are restricted to picking a single Output type. Were Output a type parameter, we could impl Add for A and B in a myriad of ways.

For example, let us define a Mul trait where Output is a parameter:

trait Mul<RHS, Output> {
    fn mul(self, rhs: RHS) -> Output;
}

Now let us define a Complex type:

#[derive(Debug, Clone, Copy)]
struct Complex {
    x: f64,
    y: f64,
}

impl Complex {
    fn new(x: f64, y: f64) -> Complex {
        Complex { x: x, y: y }
    }
}

We want to be able to multiply it by f64:

impl Mul<f64, Complex> for Complex {
    fn mul(self, rhs: f64) -> Complex {
        Complex::new(self.x * rhs, self.y * rhs)
    }
}

This all works fine. However, we could come up with a second implementation:

impl Mul<f64, f64> for Complex {
    fn mul(self, rhs: f64) -> f64 {
        self.x * rhs
    }
}

When we multiply a Complex by a f64 now, it is ambiguous which implementation should be called, and extra type information is needed to be supplied by the caller. By making Output an associated type, this is disallowed. The following code throws a compiler error for conflicting implementations:

impl std::ops::Mul<f64> for Complex {
    type Output = Complex;
    fn mul(self, rhs: f64) -> Complex {
        Complex::new(self.x * rhs, self.y * rhs)
    }
}

impl std::ops::Mul<f64> for Complex {
    type Output = f64;
    fn mul(self, rhs: f64) -> Complex {
        self.x * rhs
    }
}

Full example

like image 197
paholg Avatar answered Nov 03 '22 11:11

paholg


It's about who gets to specify the output type. Generally when you multiply two types, say f64 by f64, it's clear what the type of the result should be: f64. It wouldn't make sense for the caller to ask for a result type of String - so it doesn't really make sense as an input.

I can imagine some cases where you might want the choice different output, but they're a bit niche (say u32 * u32 returning the full u64 result?); plus if there were more than one option, how do you specify which one you want? While you could write Add<u32, u64>::add(a, b), it would make simple expressions like a*b*c ambiguous and make type inference much harder!

like image 4
Chris Emerson Avatar answered Nov 03 '22 11:11

Chris Emerson