Scala - Why are overloaded methods not called based on runtime class?

overloading, scala, types

Solution

Static overload resolution is easy to reason about: for the methods that are applicable, a method is selected as "more specific" just based on the signatures.

However,

scala> Seq((new A(), ShowA), (new B(), ShowB))
res0: Seq[(Base, ShowBase)] = List((A@2b45f918,ShowA$@7ee4acd9), (B@57101ba4,ShowB$@6286d8a3))

in `ShowBase` there is no overload.

scala> res0 foreach {
     | case (obj: A, showImpl) => println(showImpl.show(obj), obj.getClass.getSimpleName)
     | case (obj: B, showImpl) => println(showImpl.show(obj), obj.getClass.getSimpleName)
     | }
java.lang.InternalError: Malformed class name
  at java.lang.Class.getSimpleName(Class.java:1180)
  at $anonfun$1.apply(<console>:17)
  at $anonfun$1.apply(<console>:16)
  at scala.collection.immutable.List.foreach(List.scala:383)
  ... 38 elided

Oh yeah, don't use `getSimpleName` from Scala.

scala> res0 foreach {
     | case (obj: A, showImpl) => println(showImpl.show(obj), obj.getClass)
     | case (obj: B, showImpl) => println(showImpl.show(obj), obj.getClass) }
(Base,class $line4.$read$$iw$$iw$A)
(Base,class $line5.$read$$iw$$iw$B)

OTOH,

scala> class ShowBase extends Show[Base] {
     | override def show(obj: Base): String = "Base"
     | def show(a: A) = "A" ; def show(b: B) = "B" }
defined class ShowBase

scala> Seq((new A(), new ShowBase), (new B(), new ShowBase))
res3: Seq[(Base, ShowBase)] = List((A@15c3e01a,ShowBase@6eadd61f), (B@56c4c5fd,ShowBase@10a2918c))

scala> res3 foreach {
     | case (obj: A, showImpl) => println(showImpl.show(obj), obj.getClass)
     | case (obj: B, showImpl) => println(showImpl.show(obj), obj.getClass) }
(A,class $line4.$read$$iw$$iw$A)
(B,class $line5.$read$$iw$$iw$B)

It's easy to imagine a macro that generates the partial function for a given interface with an overloaded method `show`.

Another idea, not necessarily a great one, is to let the compiler do the selection at runtime.

This is currently awkward to demonstrate in REPL. You have to import any symbols you want to use from the objects that litter your REPL history. See the issue.

scala> def imps = $intp.definedSymbolList map (s => $intp.global.exitingTyper { s.fullName }) mkString ("import ", "\nimport ", "\n")
imps: String

scala> tb.eval(tb.parse(s"$imps ; ShowA show a"))
res15: Any = A

Hey, it worked!

Or, go into power mode, which sets the current phase at typer and gives you `intp` without the funky dollar sign. Because do we really need more dollars?

scala> :power
** Power User mode enabled - BEEP WHIR GYVE **
** :phase has been set to 'typer'.          **
** scala.tools.nsc._ has been imported      **
** global._, definitions._ also imported    **
** Try  :help, :vals, power.<tab>           **

scala> def imps = intp.definedSymbolList map (_.fullName) mkString ("import ", "\nimport ", "\n")
imps: String

scala> tb.eval(tb.parse(s"$imps ; ShowA show a"))
res17: Any = A

If you want to see your unsanitized imports:

scala> intp.isettings.unwrapStrings = false
intp.isettings.unwrapStrings: Boolean = false

scala> imps
res11: String =
"import $line2.$read.$iw.$iw.$intp
import $line3.$read.$iw.$iw.Base
import $line3.$read.$iw.$iw.A
import $line3.$read.$iw.$iw.B
import $line4.$read.$iw.$iw.Show
import $line5.$read.$iw.$iw.ShowA
[snip]

Once more:

scala> abstract class Base ; class A extends Base ; class B extends Base
defined class Base
defined class A
defined class B

scala> trait Show[T <: Base] { def show(obj: T): String }
defined trait Show

scala> class ShowBase extends Show[Base] { override def show(obj: Base): String = "Base" }
defined class ShowBase

scala> object ShowA extends ShowBase { def show(obj: A): String = "A" }
defined object ShowA

scala> :power
** Power User mode enabled - BEEP WHIR GYVE **
** :phase has been set to 'typer'.          **
** scala.tools.nsc._ has been imported      **
** global._, definitions._ also imported    **
** Try  :help, :vals, power.<tab>           **

scala> def imps = intp.definedSymbolList map (_.fullName) mkString ("import ", "\nimport ", "\n")
imps: String

scala> import tools.reflect._
import tools.reflect._

scala> val tb = reflect.runtime.currentMirror.mkToolBox()
tb: scala.tools.reflect.ToolBox[reflect.runtime.universe.type] = scala.tools.reflect.ToolBoxFactory$ToolBoxImpl@24e15d95

Did I mention the import mechanism is awkward?

scala> val a = new A
a: A = A@1e5b2860

scala> tb.eval(tb.parse(s"$imps ; ShowA show a"))
res0: Any = A

scala> ShowA show (a: Base)
res1: String = Base

scala> tb.eval(tb.parse(s"$imps ; ShowA show (a: Base)"))
res2: Any = Base

scala> val a: Base = new A
a: Base = A@7e3a93ce

scala> tb.eval(tb.parse(s"$imps ; ShowA show a"))
scala.tools.reflect.ToolBoxError: reflective compilation has failed:

reference to a is ambiguous;
it is imported twice in the same scope by
import a
and import a
  at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$ToolBoxGlobal.throwIfErrors(ToolBoxFactory.scala:315)
  at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$ToolBoxGlobal.wrapInPackageAndCompile(ToolBoxFactory.scala:197)
  at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$ToolBoxGlobal.compile(ToolBoxFactory.scala:251)
  at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$$anonfun$compile$2.apply(ToolBoxFactory.scala:428)
  at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$$anonfun$compile$2.apply(ToolBoxFactory.scala:421)
  at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$withCompilerApi$.liftedTree2$1(ToolBoxFactory.scala:354)
  at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$withCompilerApi$.apply(ToolBoxFactory.scala:354)
  at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl.compile(ToolBoxFactory.scala:421)
  at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl.eval(ToolBoxFactory.scala:443)
  ... 37 elided

So, if you decide what type you want to select on:

scala> val x: Base = new A
x: Base = A@2647e550

scala> tb.eval(tb.parse(s"$imps ; ShowA show x"))
res4: Any = Base

scala> tb.eval(tb.parse(s"$imps ; ShowA show (x.asInstanceOf[A])"))
res5: Any = A

Problem

Problem Given a simple class hierarchy ``` abstract class Base {} class A extends Base {} class B extends Base {} ``` And a typeclass ``` trait Show[T] { def show(obj: T): String } ``` With overloaded implementations ``` class ShowBase extends Show[Base] { override def show(obj: Base): String = "Base" } object ShowA extends ShowBase { def show(obj: A): String = "A" } object ShowB extends ShowBase { def show(obj: B): String = "B" } ``` When executing following test-case ``` Seq((new A(), ShowA), (new B(), ShowB)).foreach { case (obj, showImpl) => println(showImpl.show(obj), obj.getClass.getSimpleName) } ``` Should produce `(A,A) \n (B,B)`, but produces `(Base,A) \n (Base,B)` instead. Question What's going on here? Shouldn't the method with the most specific runtime type be called - Polymorphism 101? This issue looks similar to another question where a type parameter prevents the correct resolution of which method to call. However, in my case the type parameterized `show` method is provided with actual implementations in contrast to type parameterized method in the other question. Naive solution Extending the `ShowA` implementation (analogue for `ShowB`): ``` object ShowA extends ShowBase { def show(obj: A): String = "A" override def show(obj: Base): String = { require(obj.isInstanceOf[A], "Argument must be instance of A!") show(obj.asInstanceOf[A]) } } ``` gives the expected output. The problem is mixing `A` with `ShowB` will result in an exception.

Original source