Dubtious ambiguity resolution for trait fns with the same name but different prototypes

Currently the following snippet fails to compile with error[E0034]: multiple applicable items in scope

trait Trait1 {
    fn foo(&self, bar: u8);
}

trait Trait2 {
    fn foo(&self, bar: u16);
}

struct Baz;

impl Trait1 for Baz {
    fn foo(&self, bar: u8) {
        unimplemented!()
    }
}

impl Trait2 for Baz {
    fn foo(&self, bar: u16) {
        unimplemented!()
    }
}

fn main() {
    Baz.foo(1u8);
}

Playground

However there is no ambiguity here since the two functions have different prototypes.

Shouldn't the compiler be able to figure it out itself without resorting to fully qualified syntax?

Rust doesn't do method overloading. It finds methods by name, and then looks what arguments they are supposed to have. Type inference isn't a full constraint solver.

1 Like

From my understanding the arguments against method overloading are:

  1. It does not go well with generics
  2. You can name functions differently

But 2. isn't always applicable.

What if you have a struct implementing a generic trait multiple times?

What if you have a struct implementing 2 upstream traits that define methods with the same name?

You have to use fully qualified syntax everywhere wich leads to code with poor readability.

Feels a bit silly that Rust doesn't handle at least the simple cases, wich are quite common.

It's only a problem if both traits are in scope where you're calling the ambiguous name.

Yes, but Baz.foo(1) is doubly ambiguous. Given the way type inference flows in Rust, I think this is fine myself. How common are colliding trait method names where both traits are:

  1. implemented for a single type; and
  2. both in scope at the same place?
2 Likes

Note that it works if you have a single generic Trait<T> and implement Trait<u8> and Trait<u16> for Baz

trait Trait<T> {
    fn foo(&self, bar: T);
}

struct Baz;

impl Trait<u8> for Baz {
    fn foo(&self, bar: u8) {
        unimplemented!()
    }
}

impl Trait<u16> for Baz {
    fn foo(&self, bar: u16) {
        unimplemented!()
    }
}

fn main() {
    Baz.foo(1u8);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9d13b531120c148e0b5c76d1a23daf09

3 Likes

Rust has "left-swimming turbofish" for this:

<Baz as Trait1>::foo(&Baz, 1);
<Baz as Trait2>::foo(&Baz, 2);

Admittedly, the syntax is neither pretty nor obvious. However, resolution by argument types would have problems of overloading. e.g. what if the traits are fn foo(&self, bar: impl Into<u8>) and fn foo(&self, bar: impl Into<u16>)? C++ has a whole hierarchy of types and conversions for disambiguating this, and it creates a lot of surprising situations.

2 Likes

This is incorrect, as noted by other posters. What the OP is asking for is closer to argument-dependent lookup, which is stronger than overloading.

A more accurate statement is that Rust's overload (read: trait impl) resolution algorithm does not rank overloads and gives up if the constraint solver does not come up with a unique answer to the query; C++ will try to pick the "most specific" overload.

You can observe function overloading at work by writing x << 1u8 and x << 1u32. These expand to different calls of Shl::shl().

1 Like

There is an RFC to use impl Trait in let bindings. The implementation is apparently removed from nightly, but it can be somewhat emulated.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ddd91cef3aa67b3a8e1e9cafbc0c219d

That's not overloading, instead you should think of Shl::shl as being a single generic function

fn shl<A, B>(a: A, b: B) -> A::Output
where
    A: Shl<B,
{ … }

Well perhaps it is overloading, depending on what you define as “overloading”, at least traits (i. e. “type classes”) were indeed (IIRC) initially invented in order to “overload” syntax like < or ==.

1 Like

At the end of the day, this whole conversation is about splitting hairs. It doesn't really matter what it's called.