Fabian Schmitthenner
Fabian Schmitthenner

Reputation: 1706

Why does overloading polymorphic methods with different upper bounds not compile in Scala

Why does the following not compile in Scala:

class A
class B

object X {
  def f[Q <: A](q: Q): Q = q
  def f[Q <: B](q: Q): Q = q
}

with error message

<console>:16: error: method f is defined twice
  conflicting symbols both originated in file '<console>'
         def f[Q <: B](q: Q): Q = q

To my understanding, after type erasure, def f[Q <: A](q: Q): Q should be replaced with its upper bound: def f(q: A): Any and the second overloaded f correspondingly. So they should be distinguishable after type erasure.

So why does Scala complain anyway?

Upvotes: 7

Views: 201

Answers (4)

dk14
dk14

Reputation: 22374

Just to complement @chengpohi answer, you can actually implement static dispatching (overloading is a particular case) with type-classes:

trait A
trait B

implicit class RichA[Q <: A](q: Q){ def f = q }

implicit class RichB[Q <: B](q: Q){ def f = q }

scala> (new A{}).f
res0: A = $anon$1@39c1fe0b

scala> (new B{}).f
res1: B = $anon$1@20011bf

The reason why it doesn't work naturally is only that Scala has to mimic Java's overloading (with its erasure) to keep code compatible with external Java-code and Scala's internal features and guarantees. Overloading in your case (but not always) is basically a static call, so it can be processed in compile-time, but JVM's invokestatic does dispatching in runtime unfortunately:

Before performing the method invokation, the class and the method identified by are resolved. See Chapter 9 for a description of how methods are resolved.

invokestatic looks at the descriptor given in , and determines how many arguments the method takes (this may be zero). It pops these arguments off the operand stack. Then it searches the list of static methods defined by the class, locating the method methodname with a descriptor descriptor.

So, regardless that it knows about Q <: A restriction - it doesn't know about formal type of Q in the runtime, so some cases like that one pointed by @chengpohi seem to not possible to detect or resolve (actually they could do it based on the info from linearization - the only disadvantage is the involvement of runtime-type into dispatching).


Haskell, for instance, determines the right method in compile-time (as far as I know), so type-classes are able to compensate for the lack of truly static dispatching by deciding the right method to call in compile-time.

P.S. Note that in Haskell overloading is used for dynamic dispatching (pattern matching) and classes for a static one, so it's basically vise-versa in comparison to Java.

Upvotes: 5

mdm
mdm

Reputation: 3988

Re-posting comment as an answer to improve visibility.

I found this old post about what seems to be the same issue: http://www.scala-lang.org/old/node/4625.html

It seems to be a known issue with the Scala compiler, having to do more with the fact that it would be difficult to support this feature without breaking other (Scala-only) features and guarantees of the compiler. The post also shows few workarounds.

It would be very interesting if any compiler guru here on SO were able to shed some light on whether Dotty - or should I say Skala? ;) - will plan on fixing it.

Upvotes: 3

chengpohi
chengpohi

Reputation: 14217

This is not only Scala support this, Java not support this too:

def f[Q <: A](q: Q): Q = q
def f[Q <: B](q: Q): Q = q

It's equal to Java:

public <Q extend A> Q f(q: Q) { return q;}
public <Q extend B> Q f(q: Q) { return q;}

As all we know, type erasure will remove type in the runtime, for example, there is a type C is the both subtype of A and B, in the runtime, it will confuse which f should apply.

There is a code snippet maybe it's helpful to understand this:

As Scala trait:

trait A
trait B

class C extends A with B

so C is both subclass of A and B. if when pass it to f method, it will cause confusions in runtime.

so In Java is same, we can use interface to state subclass. this also will cause confusions in runtime. example:

interface C {
}
interface D {
}
static class A implements D, C {
}
public <Q extends C> Q f(Q q) {
    return q;
}
public <Q extends D> Q f(Q q) {
    return q;
}
new TestTmp().f(new A()) // Ambiguous call in here.

Upvotes: 2

Skaldarnar
Skaldarnar

Reputation: 116

I would like to point out that the above is possible in Java. It behaves as expected for a and b. I can see the problem with traits in Scala because of multiple mixins, but multiple inheritance of classes is not possible.

public class A {}
public class B {}

public class Test {
    public <Q extends A> Q f(Q q) {
        System.out.println("with A");
        return q;
    }

    public <Q extends B> Q f(Q q) {
        System.out.println("with B");
        return q;
    }

    public static void main(String[] args) {
        final A a = new A() {};
        final B b = new B() {};

        final Test test = new Test();

        test.f(a);
        test.f(b);
    }
}

Upvotes: 2

Related Questions