Reputation: 2169
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.
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.
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.
Upvotes: 3
Views: 1049
Reputation: 2169
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.
Upvotes: 1
Reputation: 39577
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
Upvotes: 5
Reputation: 9734
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)
}
Upvotes: 1