实现功能的价值平等

Implementing value equality of functions

如何重写 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's 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

您将如何实现函数相等,最好使用常规 ==

首先,函数相等不是一个简单的话题(剧透:它不能正确实现;参见例如 this question 和相应的答案),但让我们假设你的方法 "asserting same output for a hundred random inputs"足够好了。

覆盖 == 的问题是它已经为 Function1 个实例实现。所以你有两个选择:

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

两种选择都各有优缺点。

在第一种情况下,您必须将每个函数包装到您的自定义特征中,而不是使用标准 Scala Function1 特征。你这样做了,但随后你试图实现一个隐式转换,它将为你 "behind the scenes" 进行从标准 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 实例,但使用自定义方法进行相等性比较。这可以通过 typeclass 方法轻松实现:

  • 定义一个通用特征 MyEquals[A] 它将具有所需的方法(我们称之为 isEqual
  • 定义一个隐式值,实现 Function1[Int, String]
  • 的特征
  • 定义一个 helper implicit class,只要存在 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 特征)的优点是不可能的。但是,我认为使用 == 甚至都不是优势。继续阅读以找出原因。

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

因此,如果范围内不存在 Equal[Function1] 的隐式实例,我们只需提供我们自己的实例(就像我们在第二个代码片段中所做的那样),编译器将使用它。另一方面,如果 Equal[Function1] 的隐式实例已经存在于某个地方(例如在标准库中),它对我们没有任何改变——我们仍然只需要提供我们自己的,它将 "override"现有的。

现在最棒的是:这样的类型class 已经存在于两个 scalaz and cats 中。分别叫做EqualEq,他们都把自己的相等比较方法命名为===。这就是为什么我之前说我什至不会考虑使用 == 作为优势。谁还需要 ==? :) 在您的代码库中始终如一地使用 scalaz 或 cats 意味着您将在任何地方都依赖 === 而不是 ==,并且您的生活会很简单(r)。

但不要指望函数相等;整个要求很奇怪而且不好。为了提供一些见解,我假装很好地回答了你的问题,但最好的答案是 - 根本不要依赖函数相等性。