Question

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.

Was it helpful?

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

OTHER TIPS

It's not an answer to your question, looks more like a workaround:

  abstract class Base {}
  class A extends Base {}
  class B extends Base {}

  trait Show[T] {
    def show(obj: T): String
  }

  class ShowBase extends Show[Base] {
    override def show(obj: Base): String = "Base"
  }
  object ShowA extends Show[A] {
    override def show(obj: A): String = "A"
  }
  object ShowB extends Show[B] {
    override def show(obj: B): String = "B"
  }

  case class ^[T <: Base](obj: T, show: Show[T])

  Seq(^(new A(), ShowA), ^(new B(), ShowB)).foreach {
    case ^(obj, showImpl) => println(showImpl.show(obj), obj.getClass.getSimpleName)
  }

I had a fundamental error in thinking that overloaded methods are called based on dynamic binding (If you are wondering, the experience was like discovering that 2+2 is 5 instead of 4).

Thanks to som-snytt's answer and to a blog post about static and dynamic binding in Java I figured out that that is not the case. Overloaded methods are called based on static types. Overridden methods are called based on dynamic types. Hence the problem in my original question is based on static binding: som-snytt's answer explains that in more detail.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top