Prevent Mixin overriding equals from breaking case class equality

我的未来我决定 提交于 2019-12-21 17:48:03

问题


Squeryl defines a trait KeyedEntity which overrides equals, checking for several conditions in an if and calling super.equals in the end. Since super is Object, it will always fail.

Consider:

trait T { override def equals(z: Any):Boolean = super.equals(z)} }

case class A(a: Int) extends T

val a = A(1); val b = A(1)

a==b // false

Thus, if you declare

case class Record(id: Long, name: String ...) extends KeyedEntity[Long] { ... }

-- and you create several Record instances but do not persist them, their comparison will break. I found this by implementing both Salat and Squeryl back ends for the same class, and then all Salat tests fail since isPersisted from KeyedEntity is false.

Is there a design whereby KeyedEntity will preserve case class equality if mixed into a case class? I tried self-typing and parameterizing BetterKeyedEntity[K,P] { self: P => ... } for the case class type as P but it causes infinite recursion in equals.

As things stand right now, super is Object so the final branch of the overridden equals in KeyedEntity will always return false.


回答1:


The structural equality check usually generated for case classes seems not to be generated if there is an equals override. However some subtleties have to be noted.

Mixing the concept of id-based equality falling back to structural equality might not be a good idea, since I can imagine that it may lead to subtle bugs. For example:

  • x: A(1) and and y: A(1) are not yet persisted, so they are equal
  • then they get persisted, and since they are separate objects, the persistence framework may persist them as separate entities (I don't know Squeryl, maybe not an issue there, but this is a thin line to walk)
  • after persisting, they are suddenly not equal since the id differs.

Even worse, if x and y get persisted to the same id, the hashCode will differ before and after persisting (the source shows that if persisted it is the hashCode of the id). This breaks immutability, and will lead to very bad behavior (when put in maps for example). See this gist in which I demonstrate the assert failing.

So don't mix structural and id-based equality implicitly. Also see this explained in the context of Hibernate.

Typeclasses

It have to be noted that others pointed out (ref needed) that the concept of method-based equality is flawed, for such reasons (there is not only one way two thing can be equal). Therefore you can define a typeclass which describes Equality:

trait Eq[A] {
  def equal(x: A, y: A): Boolean
}

and define (possibly multiple) instances of that typeclass for your classes:

// structural equality
implicit object MyClassEqual extends Eq[MyClass] { ... }

// id based equality
def idEq[K, A <: KeyedEntity[K]]: Eq[A] = new Eq[A] {
  def equal(x: A, y: A) = x.id == y.id
}

then you can request that things are members of the Eq typeclass:

def useSomeObjects[A](a: A, b: A)(implicit aEq: Eq[A]) = {
  ... aEq.equal(a, b) ...
}

So you can decide which notion of equality to use by importing the appropriate typeclass in scope, or passing the typeclass instance directly as in useSomeObjects(x, y)(idEq[Int, SomeClass])

Note that you might also need a Hashable typeclass, similarly.

Autogenerating Eq instances

This situation is pretty similar to the Scala stdlib's scala.math.Ordering typeclass. Here is an example for auto-deriving structural Ordering instances for case classes using the excellent shapeless library.

The same would easy to be done for Eq and Hashable.

Scalaz

Note that scalaz has Equal typeclass, with nice pimp patterns with which you can write x === y instead of eqInstance.equal(x, y). I'm not aware it has Hashable typeclass, yet.



来源:https://stackoverflow.com/questions/12434798/prevent-mixin-overriding-equals-from-breaking-case-class-equality

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!