实现功能的价值平等

时间:2019-05-03 22:34:00

标签: scala equals

在特定情况下如何重写equals以检查功能的值等效性?例如,假设我们具有以下fg函数

val f = (x: Int) => "worf" + x
val g = (x: Int) => "worf" + x

我们如何使assert(f == g)通过?

我尝试扩展Function1并像这样通过生成器实现平等

trait Function1Equals extends (Int => String) {
  override def equals(obj: Any): Boolean = {
    val b = obj.asInstanceOf[Function1Equals]
    (1 to 100).forall { _ =>
      val input = scala.util.Random.nextInt
      apply(input) == b(input)
    }
  }
}

implicit def functionEquality(f: Int => String): Function1Equals = (x: Int) => f(x)

,但可能由于this而无法进行隐式转换以在==上工作。 Scalactics的TripleEquals接近了

import org.scalactic.TripleEquals._
import org.scalactic.Equality

implicit val functionEquality = new Equality[Int => String] {
  override def areEqual(a: Int => String, b: Any): Boolean =
    b match {
      case p: (Int => String) =>

        (1 to 100).forall { _ =>
          val input = scala.util.Random.nextInt
          a(input) == p(input)
        }

      case _ => false
    }
}

val f = (x: Int) => "worf" + x
val g = (x: Int) => "worf" + x
val h = (x: Int) => "picard" + x


assert(f === g) // pass
assert(f === h) // fail

您将如何实现功能相等,最好使用常规的==

1 个答案:

答案 0 :(得分:5)

首先,函数相等不是一个简单的话题(破坏器:它不能正确实现;请参见例如this question和相应的答案),但是让我们假设您的方法“将相同的输出声明一百随机输入”就足够了。

覆盖==的问题在于它已经为Function1实例实现。因此,您有两种选择:

  • 定义自定义特征(您的方法)并使用==
  • 使用操作isEqual定义类型类,并为Function1实现它

这两种选择都有权衡。

在第一种情况下,您不必使用标准的Scala Function1特性,而必须将每个函数包装到自定义特性中。这样做了,但是随后您尝试实现一个隐式转换,该转换将为您“在幕后”执行从标准Function1Function1Equals的转换。但是随着您意识到自己,那是行不通的。为什么?因为已经有==个实例的方法Function1,所以编译器没有理由开始隐式转换。您必须将每个Function1实例包装到您的自定义包装器中,以便调用覆盖的==

这是示例代码:

trait MyFunction extends Function1[Int, String] {
  override def apply(a: Int): String
  override def equals(obj: Any) = {
    val b = obj.asInstanceOf[MyFunction]
    (1 to 100).forall { _ =>
      val input = scala.util.Random.nextInt
      apply(input) == b(input)
    }
  }
}

val f = new MyFunction {
  override def apply(x: Int) = "worf" + x 
}
val g = new MyFunction {
  override def apply(x: Int) = "worf" + x
}
val h = new MyFunction {
  override def apply(x: Int) = "picard" + x
}

assert(f == g) // pass
assert(f == h) // fail

您的第二个选择是继续使用标准Function1实例,但是使用自定义方法进行相等性比较。这可以通过类型类方法轻松实现:

  • 定义一个通用特征MyEquals[A],它将具有所需的方法(我们将其称为isEqual
  • 定义一个隐式值,该值将为Function1[Int, String]实现该特征
  • 定义一个辅助隐式类,只要存在一个isEqual的隐式实现,该类将为类型为A的某个值提供方法MyEquals[A](并且我们已经在前面确定步骤MyEquals[Function1[Int, String]]有一个

然后代码如下:

trait MyEquals[A] {
  def isEqual(a1: A, a2: A): Boolean
}

implicit val function1EqualsIntString = new MyEquals[Int => String] {
  def isEqual(f1: Int => String, f2: Int => String) =
    (1 to 100).forall { _ =>
      val input = scala.util.Random.nextInt
      f1(input) == f2(input)
   }
}

implicit class MyEqualsOps[A: MyEquals](a1: A) {
  def isEqual(a2: A) = implicitly[MyEquals[A]].isEqual(a1, a2)
}

val f = (x: Int) => "worf" + x
val g = (x: Int) => "worf" + x
val h = (x: Int) => "picard" + x

assert(f isEqual g) // pass
assert(f isEqual h) // fail

但是正如我所说,保持第一方法(使用==)和第二方法(使用标准Function1特性)的优势是不可能的。但是,我认为使用==甚至不是优势。继续阅读以找出原因。

这很好地说明了为什么类型类比继承有用且更强大。与其从某个超类对象继承==并覆盖它(对于我们无法修改的类型(例如Function1)而言,这是一个问题,而是应该有一个类型类(我们将其称为Equal)它提供了许多类型的相等方法。

因此,如果范围中尚不存在Equal[Function1]的隐式实例,我们只需提供自己的实例(就像我们在第二个片段中所做的那样),编译器就会使用它。另一方面,如果Equal[Function1]的隐式实例确实已经存在(例如在标准库中),那么它对我们没有任何改变-我们仍然只需要提供我们自己的隐式实例,它将“覆盖”现有的一个。

现在是最好的部分:scalazcats中都已经存在此类类型类。分别称为EqualEq,它们都命名为相等比较方法===。这就是为什么我之前说过,我什至不考虑能够将==用作优势。谁仍然需要==? :)在您的代码库中始终使用scalaz或cats意味着您将在所有地方都依赖===而不是==,而且您的生活会很简单。

但是不要指望函数相等;整个需求很奇怪而且不好。我回答了您的问题,并假装可以提供一些见解,但最好的答案应该是-完全不依赖函数相等。