在特定情况下如何重写equals
以检查功能的值等效性?例如,假设我们具有以下f
和g
函数
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
您将如何实现功能相等,最好使用常规的==
?
答案 0 :(得分:5)
首先,函数相等不是一个简单的话题(破坏器:它不能正确实现;请参见例如this question和相应的答案),但是让我们假设您的方法“将相同的输出声明一百随机输入”就足够了。
覆盖==
的问题在于它已经为Function1
实例实现。因此,您有两种选择:
==
isEqual
定义类型类,并为Function1
实现它这两种选择都有权衡。
在第一种情况下,您不必使用标准的Scala Function1
特性,而必须将每个函数包装到自定义特性中。这样做了,但是随后您尝试实现一个隐式转换,该转换将为您“在幕后”执行从标准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]
实现该特征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]
的隐式实例确实已经存在(例如在标准库中),那么它对我们没有任何改变-我们仍然只需要提供我们自己的隐式实例,它将“覆盖”现有的一个。
现在是最好的部分:scalaz和cats中都已经存在此类类型类。分别称为Equal
和Eq
,它们都命名为相等比较方法===
。这就是为什么我之前说过,我什至不考虑能够将==
用作优势。谁仍然需要==
? :)在您的代码库中始终使用scalaz或cats意味着您将在所有地方都依赖===
而不是==
,而且您的生活会很简单。
但是不要指望函数相等;整个需求很奇怪而且不好。我回答了您的问题,并假装可以提供一些见解,但最好的答案应该是-完全不依赖函数相等。