实现函数值相等性

3
如何覆盖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)

但是在 == 上无法实现隐式转换,可能是因为这个原因。Scalactic 的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

您如何实现函数的相等性,最好使用普通的==操作符?

2
只是指向 Rice's Theorem,它解释了为什么你的问题是不可判定的。@slouc 的链接问题解释了其余部分。 - lambda.xy.x
1个回答

5
首先,函数相等性不是一个简单的话题(剧透:它无法被正确实现;请参见例如this question和相应的答案),但让我们假设你的“对一百个随机输入进行断言相同输出”的方法足够好。
覆盖==的问题在于它已经为Function1实例实现了。所以你有两个选择:
  • 定义一个自定义特质(你的方法)并使用==
  • 定义一个类型类,其操作为isEqual并将其实现为Function1
这两种选项都有权衡之处。
在第一种情况下,您需要将每个函数包装到自定义trait中,而不是使用标准的Scala Function1 trait。您已经这样做了,但是接着您尝试实现一个隐式转换,让它在“幕后”为您执行从标准Function1到Function1Equals的转换。但是正如您自己意识到的那样,这是行不通的。为什么?因为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]定义一个隐式值,该值实现了该特质
  • 定义一个帮助器隐式类,只要存在MyEquals[A]的隐式实现(并且我们在前一步中确保MyEquals[Function1[Int, String]]有一个实现),就会为类型为A的某个值提供isEqual方法
然后代码看起来像这样:
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 trait)的优点是不可能的。然而,我认为使用==甚至不是一个优点。接下来请继续阅读,了解原因。
这很好地展示了类型类为什么有用,比继承更强大。我们不应该从某个超类对象中继承==并覆盖它,这对于我们无法修改的类型(例如Function1)是有问题的。相反,应该有一个类型类(让我们称之为Equal),为许多类型提供相等性方法。
因此,如果在作用域中不存在Equal [Function1]的隐式实例,我们只需提供自己的实例(就像在我的第二个片段中所做的那样),编译器将使用它。另一方面,如果已经存在Equal [Function1]的隐式实例(例如在标准库中),则对我们来说什么也不会改变 - 我们仍然只需要提供自己的实例,它将“覆盖”现有的实例。
现在最好的部分:这种类型类已经存在于scalazcats中。它们分别被称为EqualEq,它们都将它们的相等比较方法命名为===。这就是为什么我之前说过,我甚至不认为能够使用==是一个优势。谁需要==呢?:) 在代码库中一致地使用scalaz或cats意味着您将在所有地方都依赖于===而不是==,这样您的生活会更加简单。

但不要指望函数相等;整个要求很奇怪,也不好。我回答你的问题只是假装没事,以提供一些见解,但最好的答案是-根本不要依赖函数相等。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接