Search code examples
scalainternalsscala-2.11

canEqual() in the scala.Equals trait


From the source code scala/Equals.scala (here):

package scala
trait Equals extends scala.Any {
  def canEqual(that: scala.Any): scala.Boolean
  def equals(that: scala.Any): scala.Boolean
}

In the documentation, it says:

A method that should be called from every well-designed equals method that is open to be overridden in a subclass.

I randomly picked a class which extends scala.Equals and which is simple enough to understand. I picked scala.Tuple2[+T1, +T2], which extends the trait scala.Product[T1, T2], which in turn extends the trait scala.Product, which in turn extends the trait scala.Equals.

Unfortunately, it seems that because scala.Tuple2 is a case class, the canEqual() and equals() methods are automatically generated and therefore could not be found in the source code scala/Tuple2.scala (here).

My questions are:

  • When is it a good time to extend the trait scala.Equals?
  • How should canEqual() be implemented?
  • What are the best practices (or boilerplate) to use canEqual() in equals()?

Thanks in advance!

PS: In case if it matters, I'm using Scala 2.11.7.


Solution

  • The canEquals method is used to cover the expectation that equals should be symmetric - that is, if (and only if) a.equals(b) is true, then b.equals(a) should also be true. Problems with this can arise when comparing an instance of a class with an instance of a sub-class. Eg.

    class Animal(numLegs: Int, isCarnivore: Boolean) {
      def equals(other: Any) = other match {
        case that: Animal => 
          this.numLegs == that.numLegs && 
          this.isCarnivore == that.isCarnivore
        case _ => false
      }
    }
    
    class Dog(numLegs: Int, isCarnivore: Boolean, breed: String) extends Animal(numLegs, isCarnivore) {
      def equals(other: Any) = other match {
        case that: Dog => 
          this.numLegs == that.numLegs && 
          this.isCarnivore == that.isCarnivore &&
          this.breed == that.breed
        case _ => false
      }
    }
    
    val cecil = new Animal(4, true)
    val bruce = new Dog(4, true, "Boxer")
    cecil.equals(bruce) // true
    bruce.equals(cecil) // false - cecil isn't a Dog!
    

    To fix this, ensure the two entities are of the same (sub-)type using canEqual in the definition of equals:

    class Animal(numLegs: Int, isCarnivore: Boolean) {
      def canEqual(other: Any) = other.isInstanceOf[Animal]
      def equals(other: Any) = other match {
        case that: Animal => 
          that.canEqual(this) &&
          this.numLegs == that.numLegs && 
          this.isCarnivore == that.isCarnivore
        case _ => false
      }
    }
    
    class Dog(numLegs: Int, isCarnivore: Boolean, breed: String) extends Animal(numLegs, isCarnivore) {
      def canEqual(other: Any) = other.isInstanceOf[Dog]
      def equals(other: Any) = other match {
        case that: Dog => 
          that.canEqual(this) &&
          this.numLegs == that.numLegs && 
          this.isCarnivore == that.isCarnivore &&
          this.breed == that.breed
        case _ => false
      }
    }
    
    val cecil = new Animal(4, true)
    val bruce = new Dog(4, true, "Boxer")
    cecil.equals(bruce) // false - call to bruce.canEqual(cecil) returns false
    bruce.equals(cecil) // false