【问题标题】:Override equality for floating point values in Scala覆盖Scala中浮点值的相等性
【发布时间】:2014-11-21 03:22:57
【问题描述】:

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

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 个对象时都会使用它。

【问题讨论】:

    标签: scala


    【解决方案1】:

    我认为将相等的含义改为模糊的含义是个坏主意。请参阅Equals for case class with floating point fields 中的我的 cmets 了解原因。

    但是,在非常有限的范围内执行此操作是有意义的,例如用于检测。我认为对于数值问题,您应该考虑使用 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))
    }
    

    【讨论】:

    • 感谢代码示例!所以基本上没有办法调整 == 的内部使用来设置相等,但是我可以导入一个重新定义相等的整个库?如果已经有像元组和数组(希望是 Seq 和 Map)这样的常见集合的实现,那么它可能是有意义的。将查看该库。我理解你的意思了吗?
    • 不完全是。这个想法是继续使用 scala 语言和核心库结构(元组、数组、序列),并使用基于类型类的库(如 spire)的机制来提供自定义相等运算符“===”,您可以调整它来做什么你要。请注意,在上面的示例中,这些是标准的 scala 元组和 (java) 数组,而不是一些自定义类型。他们自己没有定义 === 。 === 是在事后添加的。该方法也适用于 scala Seq。但是,我认为您应该忘记使用带有模糊相等概念的 Set。这基本上是不可能的。
    • 好的,是的,这就是我所理解的。至于不可能,我不希望它太模糊(只是为了不因简单的舍入错误而失败),对于大多数测试来说它可能没问题并且不会导致不匹配,但我明白你的意思。谢谢!
    • 为了比较,有点模糊是可以接受的。除了像 a==b, b==c, a!=c 这样的奇怪的东西,你将不可避免地得到。但是对于哈希表(set 或 map 键)中的存储,即使是一点点模糊也太多了。例如。 a==b,但是 set.contains(a) != set.contains(b)。因此,您可以将东西存储在您的集合中,并且永远不会再找到它们。 spire 中有很多有趣的类型,它们的行为不及 double 类型。例如 FixedPoint,它可以存储小数,但在舍入方面更具确定性。
    【解决方案2】:

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

    使用Shapeless'automatic type class instance derivation,编写一个新的类型类并为案例类递归派生实例非常容易。不过,我不确定它是否扩展到双参数类型类。您可能会发现最好创建不同的EqualityLHSEqualityRHS 类型类,然后使用您自己的相等方法来比较A: EqualityLHSB: EqualityRHS,如果需要,可以将其作为运算符添加到A。 (当然,应该可以扩展该技术以完全通用地支持双参数类型类,而不需要这样的变通方法,我相信 shapeless 会非常感谢这样的贡献)。

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

    【讨论】:

    • 嘿!感谢您的回答,投了票。问题是,如果我定义自己的比较方法(=== 或类似方法),则 Set.equals 不会调用它。如果我定义一个隐式转换,Scala 将不需要它。如果我改变一侧的类型,另一侧的基本类型可能被用来调用equals()。将研究自动类型类实例派生,以前从未听说过!
    • 是的,你必须通过Set 之类的方式将它一直向上传播,并使用你的新平等来比较你的Sets。正如你所说,Set.equals 已经定义并且已经调用了.equals,可能是在原始类型上,所以没有办法重新定义它(没有像重写整个调用链的宏之类的东西)。
    猜你喜欢
    • 2013-03-15
    • 2018-04-03
    • 2011-04-19
    • 2016-02-23
    • 2018-12-10
    • 1970-01-01
    • 2017-10-18
    • 2017-01-11
    • 2023-04-09
    相关资源
    最近更新 更多