Discussion about overloading (forked from Pre-RFC: Named arguments)

The discuttion if the Pre-RFC: Named arguments started to drift around the topic of overloading, so I’m forking it. Even if this post would probably be a better fit in URLO, I’m posting it here since I have a question directly related with this discussion (and I didn’t wanted to break the hyperlink to the original quote).

Some context:

I think there is something that I don’t understand. Why is ADL required for overloading?

Rust doesn’t have inheritance, this means that you don’t have the Int::toString versus Object::toString dilemma. And Rust evaluate the methods before monomorphisation. This means that if we wanted to expose overloads in a generic types, they would have to be explicitly listed in the trait(s) of the generic types. Something like:

trait HasOverload {
    fn with_overload(self: i32);
    fn with_overload(self: &'static str);
}

fn do_not_compile<T: HasOverload, Value>(t: T, v: Value) {
    t.with_overload(v);
}

This should not compile since Value could be something that isn’t i32 or &'static str and this would lead to a post-monomorphisation error. So we must restrain the set of accepted value:

enum IntOrString {
    i(i32),
    s(&'static str),
}
fn valid_use<T: HasOverload>(t: T, v: IntOrString) {
    match v {
        i(value) => with_overload(value), // call the first overload
        s(value) => with_overload(value), // call the second overload
    }
}
It’s obviously not very ergonomic, but some sort of delegation mechanism could make it better
fn valid_use<T: HasOverload>(t: T, v: IntOrString) {
    delegate!(with_overload, value);
}

Note: I tried to make value.with_overload() working (with an imaginary syntax), but it’s not possible right now since the arguments could be taken by value by one overload, immutable reference by a second, and by mutable reference by a third and we currently don’t have a way to abstract over this.

There is another possible interaction with specialization. As far as I understand specialization ensure that there is a set of function that is valid pre-monomorphisation (which ensure that you can’t have post-monomorphisation errors), but select the right one only post-monomorphisation.

trait HasOverload2 {
    fn with_overload<T>(self: T); // default version
    fn with_overload(self: i32); // specialized version
}

fn use<T: HasOverload, Value>(t: T, v: Value) {
    with_overload(42); // call the specialized version
    with_overload("str"); // call the default version
    with_overload(v); // if `v` is an `i32` call the specialized version of the overload, and the default version otherwise

Am I missing something, I don’t see how ADL is required in Rust, even with more allowed syntax for overloading.

If the entire overload set is on a single trait, then yes, name resolution does not rely on argument types, as the overload set can be selected by just (receiver dependent) name lookup.

But we allow constructing an ADL dependent "overload set" the same way C++ does, because multiple disparate providers can put names into the same namespace.

ADL free fn
// liba
fn proc(_: i32);

// libb
fn proc(_: &str);

// libc
proc(0); // liba::proc
proc(""); // libb::proc
ADL method
// liba
trait LibA { fn proc(&self, _: i32); }

// libb
trait LibB { fn proc(&self, _: &str); }

// libc
receiver.proc(0); // liba::LibA::proc
receiver.proc(""); // libb::LibB::proc

The issue (primarily) isn't in the overload sets which Rust prevents you from defining as a name conflict; it's in the "overload sets" of the same imported name from different namespaces that Rust currently does allow you to define but refuses to do the ADL to resolve even if such would be unambiguous.

This is where the "Rust does do ADL, but just on the method receiver" argument comes from; Rust does allow resolving from multiple conflicting local (method) names based on the type of the receiver. (The canonical example being able to define the same inherent method name differently on Local<i32> and Local<&str>.)

Part of the confusion here may be that this has very little to do with what C++ calls “argument-dependent lookup”. C++ ADL is about finding candidates to put in an overload set; the “ADL” referenced here is about using argument types to choose from an overload set.

5 Likes

I took a bit of time to think about your answers.

As far as I can tell, @RalfJung claim that ADL doesn’t exist is Rust is false, so Rust effectively has some form of overloading/can have with minor extensions.

However ADL isn’t an issue in Rust unlike in C++ because ADL in Rust is there only to filter out potential valid overload (since the full list is already declared explicitly through traits or normal visibility rules), while in C++ ADL can cause post-monomorphisation errors because C++ ADL interact with visibility.