Help to understand which method is called

Here’s a very simplified example on how method invocation works:

class A {
  override def equals(that: Any): Boolean = true
}

class B {
  // program works the same even if below code line is commented out
  override def equals(that: Any): Boolean = false
}

val a = new A
val b = new B

println(a.equals(b)) // prints: true
println(b.equals(a)) // prints: false

// upcasting to 'Object" doesn't change anything in the output
// because 'equals' method is non-static
// so the same method implementation will be invoked

val o1: Object = a
val o2: Object = b

println(o1.equals(o2)) // prints: true
println(o2.equals(o1)) // prints: false

equals is a method just like any other and follows the same rules. You don’t get automatic symmetry for any custom method implementation, so why equals should automatically get one?

== operator in Scala delegates to equals after checking for nulls. == operator in Java is equivalent to eq method in Scala. Default equals is equivalent to eq (except that equals can throw NullPointerException).

But then I won’t get pattern matching either. My applications uses pattern matching heavily on BddNode. Perhaps I want to avoid the case class, but instead write an unapply method?

Yes, adding an unapply method sound better than having a case class with reference based equality.

So the method which is called is always computed according to the runtime class of the value to the left of the dot. However, this is not the case for the rest of the method arguments. In CLOS the same rules apply to all the arguments, even the first (the object to the left of the dot, as there is no left-of-the-dot in CLOS).

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(
  List(x.m(x), // 1
       x.m(y), // 1
       x.m(z)),  // 1
  List(
    y.m(x),  // 2
    y.m(y),  // 3 
    y.m(z)),  // 2, in CLOS this would be 3
  List(
    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 

If I change this to a non-case class but with an unapply method, then can I be sure that equivalence checking within hash lookups NEVER traverses the tree of objects? Something like hash.get((bdd1,bdd2)) or hash((bdd1,bdd2)) = bdd3, I never want that to descend the BddNode object stores in the instance variables of bdd1 and bdd2. And if I create a set Set(bdd1,bdd2) then the equivalence check never looks at the instance variables of bdd1 and bdd2 ?

If I am the only one calling ==, then I can of course just call eq instead. But the point of providing a protocol for the class is that other function (that I didn’t write) may call ==.

I think this is perfectly fine if you want case-class advantaces (pattern-matching, avoid new() etc.) but want “reference-equalty”. I’d write equals() as follows tho:

override def equals(that: Any): Boolean = this eq that

avoids pattern-matching preserving semantics.

1 Like

@andreak, I like your solution, but I seem to recall NOT using that because of something that I have now forgotten.

I think I need to test this in my code base.

It’s because eq is only available on AnyRef.

ahhh, so the suggestion of @andreak won’t work then?

Both the mutable and immutable HashMap in 2.12 and earlier perform equality checks once you hit the right hash bucket. The completely new implementations in 2.13 always compare the hashcodes first. This should lower the number of equality checks.

Is there a discussion of this 2.13 HashMap change somewhere where I can read more details?

Yes it will, with a minor addition:

override def equals(that: Any): Boolean = this eq that.asInstanceOf[AnyRef]

For every practical means this is sufficient.

what would be the semantics of this?

  override def equals(obj:Any):Boolean = {
    super.equals(obj)
  }

If none of your superclasses override equals it should be the same as this eq that.asInstanceOf[AnyRef].

See https://github.com/scala/scala/pull/7348 for the new mutable HashMap and https://github.com/scala/scala/pull/6975 for cached hashcodes in the new immutable HashMap.

1 Like

That effectively changes nothing, except that your stacktraces that cross that method implementation will be longer.

Well one thing it does do is prevents scala from writing another equals method for the case class.

Is this statement still true in light of the other discussion? in Dynamic dispatch on object, but static …

Yes, that’s right. I’ve forgot about things automatically generated by Scala compiler.

I was somewhat imprecise when writing that statement. It’s true if you don’t combine overloading with upcasting (unless I forgot something) and can be false otherwise. That other discussion clears thing up.

1 Like