Proposal: make overload resolution bind more closely for matching types

This was motivated by How to disambiguate Java overloads taking long and Object? - Question - Scala Users. I found that due to the way scalac chooses overrides of overloaded methods, it makes certain method calls impossible that are possible from Java. In general, to make Java libraries usable from Scala the same way they are from Java, Scala should attempt to match Java’s overload resolution strategy. Towards this end I propose changing Expressions from

The relative weight of an alternative A over an alternative B is a number from 0 to 2, defined as the sum of

1 if A is as specific as B, 0 otherwise, and

to

The relative weight of an alternative A over an alternative B is a number from 0 to 3, defined as the sum of

2 if A is as specific as B, 0 otherwise, and

I have this working at GitHub - shawjef3/scala at 06d828a111dc5e5506892cb02fc7d8454ebad52f.

Crucially, I believe that when calling an overloaded method, if there is an option that matches my type exactly, that should be preferred over one that is defined in a subclass and does not match the type exactly. The following compiles with Java 8. Note that it doesn’t matter which class overrides methods that take which type.

class Demo0 {

    static class X {}

    static class Y extends X {}

    static class Parent {
        void f(X x) {
            System.out.println("Parent");
        }
        void f(Y y) {
            System.out.println("Parent");
        }
    }

    static class ChildOverridesX extends Parent {
        void f(X x) {
            System.out.println("ChildOverridesX");
        }
    }

    static class ChildOverridesY extends Parent {
        void f(Y y) {
            System.out.println("ChildOverridesY");
        }
    }

    static class ChildOverridesBoth extends Parent {
        void f(X x) {
            System.out.println("ChildOverridesBoth");
        }
        void f(Y y) {
            System.out.println("ChildOverridesBoth");
        }
    }

    public static void main(String[] args) {
        X x = null;
        Y y = null;

        new Parent().f(x);
        new ChildOverridesX().f(x);
        new ChildOverridesY().f(x);
        new ChildOverridesBoth().f(x);

        new Parent().f(y);
        new ChildOverridesX().f(y);
        new ChildOverridesY().f(y);
        new ChildOverridesBoth().f(y);
    }

}

The output matches what I expect.

Parent
ChildOverridesX
Parent
ChildOverridesBoth
Parent
Parent
ChildOverridesY
ChildOverridesBoth

While Demo0.ChildOverridesX#f(Y) is usable from Java, it is not usable from Scala. For Scala, it does matter which class overrides methods that take which type. Because Y <: X, when calling ChildOverridesX#f(Y), ChildOverridesX#f(X) scores 1, and Parent#f(Y) scores 1. The scores match and so scalac emits an ambiguous reference error.

class Demo0Usage {
  (null: Demo0.ChildOverridesX).f(null: Y)
}

The above does not compile. However, if we increase the score of Parent#f(Y) to 2, it wins over ChildOverridesX#f(X), and so it can be called from Scala.

As a side note, Demo0 could be written in Scala with its usage written in Java, and it would behave the same. The issue is not in the implementation, but the usage.

This becomes even more confusing when the types are unrelated from a Java perspective. This is something I would expect to be found more often in Java code.

interface LongMap extends java.util.Map {
  void f(long x) {}
  void f(Object x) {}
}

From a Java perspective, these overloads are for entirely unrelated types, but from a Scala perspective, the types are related. Object becomes Any, long becomes Long. Long <: Any. Therefore, depending on what overload is overridden in a subclass, both overloads of f can score 1, and so the method call is ambiguous. Unlike the overloads with X and Y where Y <: X, the Java library author would have no reason to think these methods are any more confusing than any other overloaded methods. Scala developers currently have no way to harness such methods.

A specific example is Long2ObjectOpenHashMap#remove(Object) and Long2ObjectOpenHashMap#remove(long) from http://fastutil.di.unimi.it/docs/it/unimi/dsi/fastutil/longs/Long2ObjectOpenHashMap.html.

Certainly this can become more complicated, as Java does not care how many overloads you create that take parameters with different, but related types, or which class or interface in a hierarchy defines such methods. Even with this proposal applied, I suppose I could still write code where methods available to Java were unavailable to Scala, and so a comprehensive fix needs more attention.

3 Likes

There’s a rather high price to pay for this. Compare the complexity of the two documents. Scala’s rules are already hairy. Java’s rules are an order of magnitude more complicated.

As a side note, even reflective access to f fails:

  def main(args: Array[String]): Unit = {
//    (null: Demo0.ChildOverridesX).f(null: Y)
    (new ChildOverridesX: { def f(x: X): Unit }).f(null: Y)
  }

gives:

Exception in thread "main" java.lang.NoSuchMethodException: overloading.Demo0$ChildOverridesX.f(overloading.Demo0$X)

To resolve ambiguity error, you can upcast the receiver:

  def main(args: Array[String]): Unit = {
//    (null: Demo0.ChildOverridesX).f(null: Y)
    (new ChildOverridesX: Parent).f(null: Y)
  }

That helps in this case.

I agree it would be a lot to match Java’s rules. It’s not really necessary. I just want a way to call methods that Java can.

To resolve ambiguity error, you can upcast the receiver:

Thank you! This is exactly what I’m looking for. Since what I want is possible I retract this whole proposal. I’ll post your solution on my original thread.

1 Like