Home > Software engineering >  Why can't type coercions occur in fn of trait with generic type automatically?
Why can't type coercions occur in fn of trait with generic type automatically?

Time:09-20

Rust support type coercion for arguments for function calls, such as

fn foo(_: &usize) {}

fn main() {
  let mut v = 10usize;
  let v_mut_ref = &mut v;
  foo(v_mut_ref);
}

But it is not working in function of trait with generic type

fn impl_add() {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    struct Number(u64);

    impl Add for Number {
        type Output = Number;
        fn add(self, other: Number) -> Number {
            Number(self.0   other.0)
        }
    }

    impl Add<&Number> for Number {
        type Output = Number;
        fn add(self, other: &Number) -> Number {
            Number(self.0   other.0)
        }
    }

    let one = Number(1);
    let mut two = Number(2);

    let two_ref = &two;
    assert_eq!(one   two_ref, Number(3));

    let two_mut_ref = &mut two;
    assert_eq!(one.add(two_mut_ref as &Number), Number(3));

    // assert_eq!(one.add(two_mut_ref), Number(3));
    // assert_eq!(one   two_mut_ref, Number(3));
}

If I uncomment it, there will be an error like below

cannot add &mut tests::impl_add::Number to tests::impl_add::Number the trait Add<&mut tests::impl_add::Number> is not implemented for tests::impl_add::Number the following other types implement trait Add<Rhs>: <tests::impl_add::Number as Add<&tests::impl_add::Number>> <tests::impl_add::Number as Add>rustcE0277

In my opinion, it is possible to first check if there is an implementation of Add<&mut Number>, not to look for Add<&Number>

Why it doesn't work.

CodePudding user response:

Essentially, coercions only happen if the target type is known.

When the compiler sees an expression like Number &mut Number, its first job is to find candidate implementations for the Add trait. This is a straight-forward lookup in this case because Add takes ownership of self, but it could be much more complicated in situations like one.add(two) where you do have to consider auto-derefs and inherent vs trait methods. But I diregres. The candidate implementations in this case would be Add<Number> and Add<&Number>.

Once candidate implementations are found, it tries to match the types. If there is a single candidate, the compiler will attempt coercion to match the types. You can see that in this Q&A: Why does adding a second impl prevent deref coercion of the argument?. However if there are multiple candidates, the type must match one of the candidates exactly. End of story.

You may ask why can't it try anyway? Well there are a lot of different types of coercions that can occur. The most troubling of which to a compiler is unsized coercions specifically to trait objects, of which there is an unbounded many possible traits that a type could implement. So exploring the coerce-able space is out of the question.

Can't it try matching the type directly against the candidates? Well the problem with that is those candidates may themselves be generic, you would once again have to try to explore an unbounded set of possible target types to try to find a match. Even if there were a single candidate, it can fail at this stage. So that again is out of the question.

There are certainly ways you could try to wiggle out of this as a compiler designer. You could choose to only consider a small subset of possible coercions (like just deref and pointer-conversions). But there could still be multiple viable targets; would you try to disambiguate them via "ranking"? (that's what C does but I don't recommend trying to fully comprehend it)

All in all, any proposed way around the problem would require much more careful consideration and would likely introduce more quirks and convoluted rules to an already complicated system. Its probably better just to require the developer be explicit in cases like this than to try to make rules to guess what they wanted.

  • Related