覆盖Scala中浮点值的相等性

时间:2014-11-19 18:20:07

标签: scala

注意:请耐心等待,我不会问如何覆盖等于或如何创建自定义方法来比较浮点值。

Scala非常适合允许按值比较对象,并提供一系列工具来完成这些操作。特别是案例类,元组和允许比较整个集合。

我经常调用进行密集计算并生成非平凡数据结构的方法来返回,然后我可以编写一个单元测试,给定某个输入将调用该方法然后将结果与硬编码进行比较值。例如:

def compute() =
{
   // do a lot of computations here to produce the set below...
   Set(('a', 1), ('b', 3))
}

val A = compute()
val equal = A == Set(('a', 1), ('b', 3))  
// equal = true

这是一个简单的例子,我在这里省略了来自特定测试库等的任何代码。 鉴于浮点值不能与equals进行可靠的比较,以下等同的示例将失败:

def compute() =
{
   // do a lot of computations here to produce the set below...
   Set(('a', 1.0/3.0), ('b', 3.1))
}

val A = compute()
val equal2 = A == Set(('a', 0.33333), ('b', 3.1))  // Use some arbitrary precision here
// equal2 = false

我想要的是有一种方法可以在该调用中进行所有浮点比较,以使用任意级别的精度。但请注意,我无法控制(或想要以任何方式改变)Set或Double。

我尝试定义从double到新类的隐式转换,然后重载该类以返回true。然后我可以在我的硬编码验证中使用该类的实例。

implicit class DoubleAprox(d: Double)
{
   override def hashCode = d.hashCode()
   override def equals(other : Any) : Boolean = other match {
      case that : Double => (d - that).abs < 1e-5
      case _ => false
   }
}

val equals3 = DoubleAprox(1.0/3.0) == 0.33333  // true
val equals4 = 1.33333 == DoubleAprox(1.0/3.0)  // false

但正如你所看到的,它打破了对称性。鉴于我在比较更复杂的数据结构(集合,元组,案例类)时,如果在左侧或右侧调用equals(),我无法定义先验。好像我必须遍历所有结构,然后在分支上进行单点浮点比较......所以,问题是:有没有办法做到这一点?


作为旁注:我对entire chapter on object equality和几个博客进行了很好的阅读,但它们只提供了继承问题的解决方案,并要求您基本拥有所有涉及的类并更改所有类。考虑到它试图解决的问题,所有这些看起来都很复杂。

在我看来,由于必须将方法添加到每个类并且一次又一次地永久覆盖,因此在Java中从根本上打破了这些事情之一。对我来说似乎更直观的是使用编译器可以找到的比较方法。比如说,你会提供equals(DoubleAprox,Double),并且每次你想要比较这些类的2个对象时都会使用它。

2 个答案:

答案 0 :(得分:2)

Java equals确实没有应有的原则 - 对此非常困扰的人使用像Scalaz&#39; Equal===。但即使这样也假设所涉及类型的对称性;我认为你必须编写一个自定义类型类来允许比较异构类型。

使用Shapeless&#39;编写一个新的类型类并且为案例类递归派生实例非常容易。 automatic type class instance derivation。我不确定是否扩展到双参数类型类。您可能会发现最好创建不同的EqualityLHSEqualityRHS类型类,然后使用您自己的平等方法来比较A: EqualityLHSB: EqualityRHS,这些方法可能会被绑定到{{1}如果需要,作为运算符。 (当然,应该可以扩展该技术,以完全普遍地支持双参数类型,而不需要这样的解决方法,而且我确信无形将非常感谢这样的贡献。)

祝您好运 - 希望这足以让您自己找到答案的其余部分。你想要做的事情绝不是微不足道的,但在现代Scala技术的帮助下,它应该是在可能性范围内。

答案 1 :(得分:2)

我认为改变平等的含义意味着任何模糊都是一个坏主意。请参阅我在Equals for case class with floating point fields中的评论。

然而,在非常有限的范围内进行此操作是有意义的,例如:用于检测。我认为对于数值问题,您应该考虑使用spire library作为依赖项。它包含大量有用的东西。其中包括用于相等的类型类和基于各个标量类型的类型类实例为复合类型(集合,元组等)派生类型类实例的机制。

因为你观察到,java世界中的相等性从根本上被打破了,它们正在使用其他运算符(===表示类型安全相等)。

以下是一个示例,您将如何重新定义有限范围的相等性,以便在比较测试结果时获得模糊等式:

// import the machinery for operators like === (when an Eq type class instance is in scope)
import spire.syntax.all._

object Test extends App {
  // redefine the equality for double, just in this scope, to mean fuzzy equali
  implicit object FuzzyDoubleEq extends spire.algebra.Eq[Double] {
    def eqv(a:Double, b:Double) = (a-b).abs < 1e-5
  }

  // this passes. === looks up the Eq instance for Double in the implicit scope. And 
  // since we have not imported the default instance but defined our own, this will
  // find the Eq instance defined above and use its eqv method
  require(0.0 === 0.000001)

  // import automatic generation of type class instances for tuples based on type class instances of the scalars
  // if there is an Eq available for each scalar type of the tuple, this will also make an Eq instance available for the tuple
  import spire.std.tuples._
  require((0.0, 0.0) === (0.000001, 0.0)) // works also for tuples containing doubles

  // import automatic generation of type class instances for arrays based on type class instances of the scalars
  // if there is an Eq instance for the element type of the array, there will also be one for the entire array
  import spire.std.array._
  require(Array(0.0,1.0) === Array(0.000001, 1.0)) // and for arrays of doubles

  import spire.std.seq._
  require(Seq(1.0, 0.0) === Seq(1.000000001, 0.0))
}