Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Structured equals at runtime in Scala

Tags:

scala

Is there some function in scala that will compare two objects structurally at runtime? I'd rather not have to manually create an equals function due to the number of fields. This is for a unit test, so performance isn't an issue. I just need something that reflectively looks through the fields and compares the values.

Background: I have a case class that extends from a trait that overrides equals and thus doesn't generate the structural equals (squeryl KeyedEntity, related: Prevent Mixin overriding equals from breaking case class equality), and I'm not in a position to change that right now. This is for a unit test so both instances are not persisted.

like image 212
ttt Avatar asked Oct 04 '13 16:10

ttt


1 Answers

I came up with this solution which iterates through all fields, and for private fields, finds a corresponding method in scala. Indeed, case classes have private members accessible through methods of the same name.

implicit class ReallyEqual[T](a: T) {
  import java.lang.reflect.Modifier._
  def ==:==[U](b: U): Boolean = {
    val fieldsA = a.getClass.getDeclaredFields
    val methodsA = a.getClass.getDeclaredMethods
    val mapA = (methodsA.toList map (z => (z.getName(), z))).toMap
    val fieldsB = b.getClass.getDeclaredFields
    val methodsB = b.getClass.getDeclaredMethods
    val mapB = (methodsB.toList map (z => (z.getName(), z))).toMap
    fieldsA.length == fieldsB.length &&
    (true /: (fieldsA zip fieldsB)){ case (res, (aa, bb)) =>
      if(((aa.getModifiers & STATIC) != 0) || ((bb.getModifiers & STATIC) != 0)) { res  // ignore
      } else if(((aa.getModifiers & (PRIVATE | PROTECTED)) == 0) && ((bb.getModifiers & (PRIVATE | PROTECTED)) == 0)) {
        res && aa == bb && aa.get(a) ==:== bb.get(b)
      } else if((mapA contains aa.getName) && (mapB contains bb.getName)) {
        res && mapA(aa.getName).invoke(a) == mapB(aa.getName).invoke(b)
      } else res
    }
  }
}

trait EqualBad {
  override def equals(other: Any) = false
}

case class MyCaseClass(x: Int, y: List[Int]) extends EqualBad

MyCaseClass(1, List(1, 2)) ==:== MyCaseClass(1, List(1, 2)) // true
MyCaseClass(1, List(1, 2)) ==:== MyCaseClass(1, List(2, 2)) // false
MyCaseClass(1, List(1, 2)) ==:== MyCaseClass(2, List(1, 2)) // false
MyCaseClass(1, List(1, 2)) == MyCaseClass(1, List(1, 2))  // false

You can even make it more recursive by changing the last == in the method ReallyEqual to ==:==

Careful: This will not work for integers, because Int does not have any field or methods. Ex: 1 ==:== 2 will return true.

like image 136
Mikaël Mayer Avatar answered Oct 20 '22 08:10

Mikaël Mayer