Disambiguating methods by signature

The following code, where a struct implements two traits that both require a method with the same name (fun):

trait OnlySelf {
    fn fun(&self);
}

trait SelfAndInt {
    fn fun(&self, a: usize);
}

struct S;

impl OnlySelf for S {
    fn fun(&self) {
        println!("Hello");
    }
}

impl SelfAndInt for S {
    fn fun(&self, a: usize) {
        println!("Hello, {}", a);
    }
}

fn main() {
    let a = S;
    a.fun();
    a.fun(1);
}

fails to compile with the following error:

error[E0034]: multiple applicable items in scope
  --> src/main.rs:24:7
   |
24 |     a.fun();
   |       ^^^ multiple `fun` found

The argument count of each trait's method is different however, therefore there is only one valid way in which a.fun() and a.fun(1) can be interpreted, namely a.fun() ≡ OnlySelf::fun(&a) and a.fun(1) ≡ SelfAndInt::fun(&a, 1).

Is the compiler protecting me from some sort of footgun or it isn't (yet?) smart enough to infer which method has to be used each time? Maybe inferring this can become too expensive so it is purposefully left out of rustc?

This kind of inference was deliberately left out of Rust so that the compiler can reliably catch programmer errors of giving the wrong arguments to a function. Here, the two functions are mostly equivalent in effect but that won’t necessarily be the case, especially if the two traits come from different crates.

Basically, the compiler can’t tell whether the programmer who wrote a.fun(1) meant:

  • <a as SelfAndInt>::fun(1), or
  • <a as SelfOnly>::fun(1), and was mistaken about the arguments that are required.

It also adds a bit of complexity to the compiler: As far as I know, there aren’t any other instances of miltiple-arity functions or argument-dependent inference in the language.

See also:

TBH I find that as a really poor excuse.
Compiler HAS all the knowledge it needs to properly refuse the code if the arguments are wrong for whatever reason.
From what I can gather the decision has been made to make it as explicit as possible so the PROGRAMMER not the compiler is not being confused.
Is it a bad thing? Of course not.

You may have that opinion, and that is your right.
But it doesn't really matter, because everyone who would have been bitten by such inference at any point (myself included) is rather happy that the compiler doesn't implement that (mis)feature.

Remember: just because you can doesn't mean you should.

2 Likes

How could you be bitten by such interface in Rust?

Would have, as in would have if the (mis)feature had been implemented.

1 Like

How, give example. I'm saying that with the rust type system it would be impossible for the programmer to provide wrong args by mistake.

// Logger::log() saves input in your boss's box
// and he only replies to emails on Friday 17:00

trait Numberphile {
    fn act(&self, x: usize);
}

trait Stringphile {
  fn act(&self, x: &str);
}

struct S;

impl Numberphile for S {
    fn act(&self, x: usize) {
        Logger::log("number", x);
    }
}

impl Stringphile for S {
    fn act(&self, x: &str) {
        Logger::log("string", x);
    }
}

// ...
// in a file far, far away
// ...

fn main() {
    let a = S;
    let x = 1; // Oops, I meant 1.to_string()!
    a.act(&x);
}

Rust doesn't compile this now, but it would compile it if this kind of type inference were allowed. And you'd need a lot of time to debug it. Replace Logger::log() with println!() to try it out.

Inference couldn't work if two unrelated traits provided same named methods that use the same arguments.

People more clever than me can probably think of situations where methods added to traits in a dependency could result in broken code that previously compiled, but is now ambiguous. That makes adding methods to traits a breaking change, even if you are only using the trait, not implementing it. Something it probably shouldn't be.

Trait methods are part of their interface. Changing this interface is a breaking change and I believe this is by design. When you encounter a new struct that is Display, you wouldn't want to wonder whether it implements fmt. You'd rather want to be sure that it does. Rust enforces that.

I wasn't referring to code implementing a trait. Of course they have to implement all methods. I was referring to downstream code that simply calls those methods. But in hindsight that can already break code anyway since we don't have to disambiguate when no conflicting method exists.

Example:
(Consider Blah and Meh to be separate crates this program depends on)
Adding the commented baz method to the Bar trait breaks the downstream code.
I was incorrectly thinking this would only be a problem for method inference by argument type, but it is a breaking change here as well because the call to s.baz() become ambiguous in the presence of the modified Bar trait.

use Blah::Foo;
use Meh::Bar;

fn main() {
    let s = "Hello";
    s.baz();
    s.blorb();
}

mod Blah {
    pub trait Foo {
        fn baz(&self);
    }

    impl Foo for &str {
        fn baz(&self) {
            println!("{} baz Foo!", self);
        }
    }
}

mod Meh {
    pub trait Bar {
        fn blorb(&self);
        //fn baz(&self);
    }

    impl Bar for &str {
        fn blorb(&self) {
            println!("{} blorb Bar!", self);
        }

        //fn baz(&self) {
            //println!("{} baz Bar!", self);
        //}
    }
}