实现功能的价值平等
Implementing value equality of functions
如何重写 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'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" 进行从标准 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
实例,但使用自定义方法进行相等性比较。这可以通过 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 中。分别叫做Equal
和Eq
,他们都把自己的相等比较方法命名为===
。这就是为什么我之前说我什至不会考虑使用 ==
作为优势。谁还需要 ==
? :) 在您的代码库中始终如一地使用 scalaz 或 cats 意味着您将在任何地方都依赖 ===
而不是 ==
,并且您的生活会很简单(r)。
但不要指望函数相等;整个要求很奇怪而且不好。为了提供一些见解,我假装很好地回答了你的问题,但最好的答案是 - 根本不要依赖函数相等性。
如何重写 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'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" 进行从标准 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
实例,但使用自定义方法进行相等性比较。这可以通过 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 中。分别叫做Equal
和Eq
,他们都把自己的相等比较方法命名为===
。这就是为什么我之前说我什至不会考虑使用 ==
作为优势。谁还需要 ==
? :) 在您的代码库中始终如一地使用 scalaz 或 cats 意味着您将在任何地方都依赖 ===
而不是 ==
,并且您的生活会很简单(r)。
但不要指望函数相等;整个要求很奇怪而且不好。为了提供一些见解,我假装很好地回答了你的问题,但最好的答案是 - 根本不要依赖函数相等性。