Why does the compiler fail to infer types based on known assoc types, but works otherwise based on known Self / generic type parameters?

trait MyMul<Rhs = Self> {
    type Output;

    fn mul(self, rhs: Rhs) -> Self::Output;
}

// impl 1
impl MyMul<i8> for i16 {
    type Output = i16;

    fn mul(self, rhs: i8) -> i16 {
        self * rhs as i16
    }
}

// impl 2
impl MyMul<i16> for i8 {
    type Output = i32;

    fn mul(self, rhs: i16) -> i32 {
        self as i32 * rhs as i32
    }
}

fn main() {
    // let _ = dbg!(8.mul(16));   // doesn't work
    // let _: i32 = dbg!(8.mul(16));   // doesn't work
    let _ = dbg!(8.mul(16i16));   // works
    let _ = dbg!(8i8.mul(16));   // works
    
    // with impl 1 commented out
    // let _ = dbg!(8.mul(16));   // works
    // let _: i32 = dbg!(8.mul(16));   // works (apparently)
}

It seems that with both impls existing, the compiler could find the desired impl based on Self type / generic type paremeter Rhs only, but not based on assoc type Output.

Why?

To prove that the compiler didn't fail because the known type i32 was the returned type, I twisted the trait definition a little:

pub trait MyAdd<Output = Self> {
    type Rhs;

    fn add(self, rhs: Self::Rhs) -> Output;
}

// impl 3
impl MyAdd<i16> for i16 {
    type Rhs = i8;

    fn add(self, rhs: i8) -> i16 {
        self + rhs as i16
    }
}

// impl 4
impl MyAdd<i32> for i8 {
    type Rhs = i16;

    fn add(self, rhs: i16) -> i32 {
        self as i32 + rhs as i32
    }
}

fn main() {
    // let _ = dbg!(8.add(16));   // doesn't work
    let _: i32 = dbg!(8.add(16));   // works
    // let _ = dbg!(8.add(16i16));   // doesn't work
    let _ = dbg!(8i8.add(16));   // works

    // with impl 3 commented out
    // let _ = dbg!(8.add(16));   // works
    // let _ = dbg!(8.add(16i16));   // works (apparently)
}

This isn't an answer as to why, but the trait parameters and implementing type are considered inputs to an implementation, whereas associated types are considered outputs (they are determined by the inputs). The compiler is attempting to choose the appropriate implementation based on the inputs.

The single implementation versions work because the compiler will just choose that implementation once it decides it's the only candidate. It's still not looking at the associated type to find the implementation. For example, this says "the types don't match", not "I couldn't find an applicable implementation".

Your examples are somewhat complicated by integers having a fall-back type: if it finds multiple possible implementations while an integer is ambiguous, it decides the integer is an i32, and then succeeds or fails based on that. (Tangential "surprising inference" topic.)

2 Likes

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.