Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 359
Fabian Schmitthenner Avatar asked Apr 05 '17 09:04

Fabian Schmitthenner


2 Answers

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.

like image 21
mdm Avatar answered Sep 29 '22 09:09

mdm


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.

like image 158
dk14 Avatar answered Sep 29 '22 11:09

dk14