Scala with Cats 学习笔记2-Cat 的 Eq 实现

Abstract: 继续阅读 Scala with Cats,我们使用 Eq 来进一步巩固我们前面学到的 Type Class 的设计模式。Eq 的用途很简单,就是定义两种类型相等的条件。

Cat Eq 使用

目标:实现两个同类型的对象的比较。

1
2
3
val obj1 = MyObj()
val obj2 = MyObj()
obj1 === obj2

Java 我们熟悉使用 .equal 方法来实现对象的比较,这里引入 Type Class 使用更加简洁。

案例1

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import cats.instances.int._    // for Eq
import cats.instances.string._ // for Eq
import cats.Eq
import cats.syntax.eq._ // for ===

implicit val catEqual: Eq[Cat] =
Eq.instance[Cat] { (cat1, cat2) =>
(cat1.name === cat2.name ) &&
(cat1.age === cat2.age ) &&
(cat1.color === cat2.color)
}
val cat1 = Cat("Garfield", 38, "orange and black")
// cat1: Cat = Cat(Garfield,38,orange and black)

val cat2 = Cat("Heathcliff", 32, "orange and black")
// cat2: Cat = Cat(Heathcliff,32,orange and black)

cat1 === cat2
// res17: Boolean = false

Show 一样,我们只需要实现个对应类型的 Instance 即可,其中需要依赖一些基础类型的 Instance

案例2

该案例实现两个 Date 类型的对象的比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.Date
import cats.instances.long._ // for Eq

implicit val dateEq: Eq[Date] =
Eq.instance[Date] { (date1, date2) =>
date1.getTime === date2.getTime
}

val x = new Date() // now
val y = new Date() // a bit later than now

x === x // res13: Boolean = true
x === y // res14: Boolean = false

Cat Eq 源码分析

Type Class 定义

1
2
3
4
5
6
7
8
9
10
11
12
trait Eq[@sp A] extends Any with Serializable { self =>

/**
* Returns `true` if `x` and `y` are equivalent, `false` otherwise.
*/
def eqv(x: A, y: A): Boolean

/**
* Returns `false` if `x` and `y` are equivalent, `true` otherwise.
*/
def neqv(x: A, y: A): Boolean = !eqv(x, y)
}

接口的实现

我们在上一篇笔记中学习这一步需要进行接口的实现,由于 Cat 希望提供抽象的支持,所以它只提供基础的数据类型的实现,并且提供一系列的工厂方法帮助用户在客户端创建自定义类型的 instance

基础类型 instance

Cat 并没有把每个 Type Class 的基础类型的实现分开,而是统一为基础类型的 Instance。我们使用 String 这个基础类型举例,其 Instance 的实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
object string          extends StringInstances

trait StringInstances extends cats.kernel.instances.StringInstances {
implicit val catsStdShowForString: Show[String] =
Show.fromToString[String]
}

trait StringInstances {
implicit val catsKernelStdOrderForString: Order[String] with Hash[String] = new StringOrder
implicit val catsKernelStdMonoidForString: Monoid[String] = new StringMonoid
}

class StringOrder extends Order[String] with Hash[String] {

def hash(x: String): Int = x.hashCode()

override def eqv(x: String, y: String): Boolean =
x == y
def compare(x: String, y: String): Int =
if (x eq y) 0 else x compareTo y
}

我们可以看到其中 eqv 方法实现:

1
2
override def eqv(x: String, y: String): Boolean =
x == y

工厂方法

object Eq 中定义了一系列的方法帮助创建 EqInstance,比如 instance

1
2
3
4
5
6
7
/**
* Create an `Eq` instance from an `eqv` implementation.
*/
def instance[A](f: (A, A) => Boolean): Eq[A] =
new Eq[A] {
def eqv(x: A, y: A) = f(x, y)
}

该方法接受一个 lambda (函数)作为参数,这个函数就是用来比较数据类型判断相等的标准。其使用方法如下: A 是我们的目标对象,我们要给这个对象增加=== 的能力,这个能力的实现就是传入的函数,比如 Date类型,我们用其 getTime 方法返回值是否相等来判断两个 Date 是否相等。

1
2
implicit val dateEq: Eq[Date] =
Eq.instance[Date]((date1, date2) => date1.getTime === date2.getTime)

注:函数调用小括号和花括号都是可以的,所以这里我们用小括号,上一节我们用的花括号。

语法 === 实现

最后就是语法糖,一般情况我们都是使用 Syntax,使用更加直观。

1
2
3
4
5
6
7
8
9
10
trait EqSyntax {
/** not final so it can be disabled in favor of scalactic equality in tests */
implicit def catsSyntaxEq[A: Eq](a: A): EqOps[A] =
new EqOps[A](a)
}

final class EqOps[A: Eq](lhs: A) {
def ===(rhs: A): Boolean = macro Ops.binop[A, Boolean]
def =!=(rhs: A): Boolean = macro Ops.binop[A, Boolean]
}

总结

我们通过 Eq 进一步巩固了 Type Class 模式,同时也学习了 Cat 的内部实现源码,对于其他 CatType Class 原理基本和 ShowEq 大同小异。