Symmetric impl of a trait - Ambiguous method call

Let f be a symmetric function of 2 variables.

Let's say I define an associated trait, F, to mark f-implementors:

trait F<T> {
    fn f(&self, other: T);
}

Now, since f is symmetric, I'd like to have the following blanket impl:

impl<T0, T1> F<T1> for T0 where T1: F<T0> {
    fn f(&self, other: &T1) {
        other.f(self)
    }
}

Obviously I can't do that because then coherence prevents any concrete impl of F.

So instead I define a trait converse to F:

trait ConF<T> where Self: Sized, T: F<Self> {
    fn f(&self, other: &T) {
        other.f(self)
    }
}

impl<T0, T1> ConF<T1> for T0 where T1: F<T0> {}

Now, consider this playground.

Somehow the compiler fails to resolve the f calls. But u8 only impl F<u16> and u16 only impl ConF<u8> so there is, in fact, no ambiguity.

Resorting to fully qualified syntax is not an option since it defeats the purpose of ConF.

What's wrong here? Is there a (better) way to achieve the desired symmetry effect?

You can use autoref specialization for this: playground. I'm not sure it's possible to get rid of the leading (&...) construction for the call, though. It's also unclear how this could be useful in practice, given that you can't be generic over both F and ConF implementors.

Yeah, specialization solves this I guess but in theory you shouldn't need it.

I'm not familiar with the monomorphization process but I think the problem comes from the way some trait bounds are checked against, namely T1: F<T0> in ConF blanket impl.

As for a use case:

Imagine a geometry library that defines some elements and an intersection function. How annoying and counterintuitive would it be if you had to figure out for every combination if it's either Elem1.intersection_with(Elem2) or the converse that is defined? You do have to duplicate some code for generic impls on F and ConF though.

Why would Elem1 and Elem2 ever be different types?

This is odd. Namely, it seems to think u8 does impl ConF<u16> by erroring on ambiguity, and if you apply that suggestion by calling ConF::f(&0u8, &0u16) the new error is about u16 not implementing F<u8>, not about u8 not implementing ConF<u16>. It's almost like it never quite rules it out.

Using Chalk doesn't seem to help with the ambiguity. If you try ConF::f(&0u8, &0u16) with Chalk, it does notice that u8 does not implement ConF<u16> though.

Edit: A variation to show the bound error is about the impl and not the trait itself.

There's an argument that this should be considered ambiguous because the blanket impl leaves the door open for u8 to indirectly implement ConF<u16>. However, if this is the intention, the error messages are still misleading and the hint incorrect.

Here's a way using associated types to limit the blanket impl.

1 Like

Well, surely a line is not a polygon (data-wise)? And surely they don't intersect like a cube and a sphere do (behavior-wise)?

Or you're suggesting dynamic dispatch which is a performance blunder in this case.

Which is weird IMO. Since in theory the only way for u8 to impl ConF<u16> is for u16 to impl F<u8> (because of the blanket impl), the compiler should be able to tell that this is not the case in pratice and rule out the blanket impl when resolving f.

Nice!

Obviously this works because we're implementing a single trait here.

The two trait approach also runs into ambiguity (instead of a compiler error) in the impl F<u32> for u32 type case.

Here's a tightened up version of the one trait approach that's probably easier for implementing consumers of the trait to understand.

2 Likes

I was suggesting dynamic dispatch. You didn't say anything about performance considerations.

1 Like

Indeed, but at least this is expected. Did not bring it up since the non-reflective case wasn't even working.

The single trait aproach is clearly superior!

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.