Dynamic dispatch on object, but static dispatch on arguments sometimes

This is a tangent from Help to understand which method is called. Can someone help me understand why z.m(y) returns 2 but y.m(y) returns 3? It seems to me that they should both return the same thing.

class X {
  def m(x:X):Int = 1
class Y  extends X {
  override def m(x:X):Int = 2
  def m(y:Y):Int = 3

val x:X = new X
val y:Y = new Y
val z:X = new Y
  List(x.m(x), // 1
       x.m(y), // 1
       x.m(z)),  // 1
    y.m(x),  // 2
    y.m(y),  // 3 
    y.m(z)),  // 2, in CLOS this would be 3
    z.m(x), // 2
    z.m(y), // 2 --  I fail to understand why this returns 2, while y.m(y) returns 3
    z.m(z))) // 2 

Well, it’s exactly as you say in the title. The JVM implements single dispatch, based on the dynamic type of the receiver object. While you seem to expect multiple dispatch.

The wikipedia article says that Scala can emulate multiple dispatch “via multi-parameter type classes”, but I invite the person who wrote that to explain how exactly. Because typeclasses are inherently static.

“expect” is probably stronger than I’d say. If it is multi dispatch I’d expect y.m(z) to return 3. However, if it’s single dispatch, I’d expect y.m(y) and z.m(y) to return the same thing. Both the declared type of the variable and the type of the actual object are both Y, known at compile time and at run time. Yet the method on ‘X’ is called instead.

The thing is that class X only has one method m(X)Int. In Y you override that method and add an overload (not override) m(Y)Int. So when you call y.m(y) statically the most specific method is selected: m(Y)Int because y has type Y and the compiler knows it has that method. When you call z.m(y) then the most specific method is m(X)Int because z had type X so the compiler doesn’t know there is a method m(Y)Int. Then at runtime dynamic dispatch kicks in and the override in class Y will be executed.

I guess that in CLOS overriding and overloading are done at the same time contrary to overriding and overloading in Scala, Java, Kotlin, etc which are done separately. What I mean is that in Scala (Java, Kotlin, etc) overloading is done at compile time, but overriding has effect only at runtime. Overloaded methods in Scala are in fact completely separate from JVM point of view. Scala compiler chooses the statically computed overload at compile time. Such computation is based on type of reference (and compile time types of arguments), not the actual type of an object (because statically you can only determine the type of reference, not the actual class of referenced object). Overriding has an effect at runtime only, because when the JVM invoke a method on an object it checks the actual class of that object and searches for the most specific override of that method in that class.

In other words: given val receiver: SuperClass = new SubClass invocation like receiver.method(args) is handled as follows:

  • at compile time Scala compiler knows the static types of receiver reference and static types of arguments. Scala compiler uses that knowledge to select the most appropriate overload from class SuperClass and saves that information in produced Java bytecode
  • at runtime JVM analyzes bytecode and executes it. When it get to the invocation in we’re discussing it sees the signature of overload chosen by Scala compiler at compile time - this is the starting point. JVM then looks in the actual class of the object pointed by receiver reference, which is Child class and looks for a most specific override compatible with previously loaded signature.

I have actually simplified the mechanism somewhat. You need to read the Scala language specification to know how Scala compiler resolve overloads. Then you need to read how Scala compiler encode Scala method signatures in Java bytecode (some information may be lost during that process). Then you need to read JVM specification to know how JVM handles overrides based on information it can decode from Java bytecode.

It is very hard for me to say because in CLOS we don’t use those terms as far as I know. Not sure why. Maybe the terms are not even applicable. In CLOS, methods are defined at compile time (or load time if your loaded a compiled file) and when the function is called an effective method is computed by combining all the applicable methods (applicable according to the types of all the [required] arguments). The standard-method-combination creates an effective method which is semantically equivalent to just calling the most specific method. And in this case the compiler might be able to pre-compute it at compile time. Certainly the 2nd time the method is called with the same types of arguments, the dispatch is fast because the effective method has already been computed and memoized.

So I don’t know how much of that protocol (if any) is consider overriding and how much is overloading.

Scala has different overloading rules (and related terminology) than Java, but in the JLS there’s interesting statement about the relationship between overloading and overriding:

Scala language specification mentions overloading and overriding, e.g.:

Let me see whether I understand your explanation. Am I correct that there are two methods. One named m(X) and one named m(Y), one has an override but exists in classes X and Y. The other has no override and exists only in class Y. It is as if I had named the two methods mx and my instead of naming them both m.

If I apply my mx/my interpretation to my original code, I indeed get answers which match my test case.

class X {
  def mx(x:X):Int = 1
class Y  extends X {
  override def mx(x:X):Int = 2
  def my(y:Y):Int = 3

val x:X = new X
val y:Y = new Y
val z:X = new Y
  List(x.mx(x), // 1
       x.mx(y), // 1
       x.mx(z)),  // 1
    y.mx(x),  // 2
    y.my(y),  // 3 
    y.mx(z)),  // 2
    z.mx(x), // 2
    z.mx(y), // 2
    z.mx(z))) // 2 

That’s a correct transformation. You’ve renamed the methods (along with their invocations) using statically available types. Then you’ve applied actual (runtime) receiver object types to determine the target override.

I was thinking about showing you that transformation for educational purposes, but I’m happy that you’ve figured it out yourself. Congrats! :slight_smile:

If you need full understanding of overloading and overriding rules then you must read carefully the language and VM specifications. I didn’t do that because the basic intuition is generally enough for me. I avoid mixing overloading and overriding on the same method. Almost always a programmer chooses one or the other, not both at the same time.

1 Like