Equals for case class with floating point fields

2019-04-01 00:20发布

问题:

Is it ok, to create case classes with floating point fields, like:

case class SomeClass(a:Double, b:Double) 

I guess auto generated equal method won't work in this case. Is overriding equals the best solution?

EDIT:

if overriding equals is the way to go, I would like to avoid hardcoding epsilon ( where epsilon is defined like => |this.a-a|< epsilon). This won't compile:

case class SomeClass(a:Double, b:Double, implicit epsilon:Double)  

I am looking for a way to pass epsilon without passing concert value each time (some "implicit" magic).

I have also follow up more general question, how would you define hashcode for class with only floating point fields?

回答1:

You are correct. If you are worried about precision, then you will need to override equals:

case class SomeClass(a:Double, b:Double)
SomeClass(2.2 * 3, 1.0) == SomeClass(6.6, 1.0)
// res0: Boolean = false

case class BetterClass(a: Double, b: Double) {
  override def equals(obj: Any) = obj match {
    case x: BetterClass =>
      (this.a - x.a).abs < 0.0001 && (this.b - x.b).abs < 0.0001   
    case _ => false
  }
}
BetterClass(2.2 * 3, 1.0) == BetterClass(6.6, 1.0)
// res1: Boolean = true


回答2:

Ah, the joy of floating point numbers.

I think it is not a good idea to override equals with a fuzzy comparison. It violates all sorts of things that you usually take for granted with equality. Imagine a, b and c are some case classes with a fuzzy equals. Then it is possible to have a, b, c such that a==b, b==c but a!=c.

Then there is the behavior of hashcode to consider. If you override equals with fuzzy equality and do not override hashcode, you will not be able to use the resulting object in a hashmap or set, because a==b but a.hashcode!=b.hashcode.

The best way to solve the problem is to define an operator like =~= that provides a fuzzy comparison in addition to equals/== which (at least for immutable objects in scala) means that objects are exactly identical so that you can replace one with the other without changing the result of a calculation.

If you also want the ability to configure the precision of the comparison via an implicit, that adds another level of complexity. Here is a more complete example:

// a class to configure the comparison that will be passed to the operator 
// as an implicit value
case class CompareSettings(epsilon:Double = 0.1) extends AnyVal

// add an operator =~= to double to do a fuzzy comparions
implicit class DoubleCompareExtensions(val value:Double) extends AnyVal {
  def =~=(that:Double)(implicit settings:CompareSettings) : Boolean = {
    // this is not a good way to do a fuzzy comparison. You should have both relative
    // and absolute precision. But for an example like this it should suffice.
    (value - that).abs < settings.epsilon
  }
}

case class SomeClass(x:Double, y:Double) {
  // we need an implicit argument of type CompareSettings
  def =~=(that:SomeClass)(implicit settings:CompareSettings) =
    // the implicit argument will be automatically passed on to the operators
    this.x =~= that.x && this.y =~= that.y
}

// usage example
val x=1.0
val y=1.01

// this won't work since there is no implicit in scope
x =~= y

// define an implicit of the right type
implicit val compareSettings = CompareSettings(0.2)

// now this will work
x =~= y

// and this as well
SomeClass(1,2) =~= SomeClass(1.1,2)

Note that the implicit is not an argument of the class but of the operation.