1. 简介
本教程将介绍 Scala Cats,这是一个为 Scala 提供类型安全、函数式编程风格抽象的强大库。Cats 包含了大量用于函数式编程的工具,主要以 类型类(type class) 的形式提供,我们可以将这些类型类应用到现有的 Scala 类型上。
2. 添加 SBT 依赖
首先,在项目中添加 Cats 的依赖:
libraryDependencies += "org.typelevel" %% "cats-core" % "2.2.0"
我们使用的是 Cats 2.2.0 版本。
3. 类型类(Type Classes)
类型类 是一种起源于 Haskell 的编程模式。它允许我们在不使用传统继承、也不修改原始库源码的前提下,为已有库扩展新功能。在 Scala Cats 中,一个类型类通常由以下几个部分组成:
- ✅ 类型类(Type Class)
- ✅ 类型类实例(Type Class Instances)
- ✅ 接口对象(Interface Objects)
- ✅ 接口语法(Interface Syntax)
3.1. 类型类定义
类型类本质上是一个接口或 API,代表我们想要实现的功能。它通过一个带有至少一个类型参数的 trait 来表示。例如,我们定义一个 Area
类型类:
trait Area[A] {
def area(a: A): Double
}
Cats 的设计思想是为每个类型提供一个通用的实现,并将必要的功能抽象成类似 Area[A]
这样的 trait。
3.2. 类型类实例
类型类实例的作用是为某个具体类型提供该类型类的具体实现。我们可以通过创建 trait 的具体实现并加上 implicit
关键字来定义实例:
case class Rectangle(width: Double, length: Double)
case class Circle(radius: Double)
object AreaInstances {
implicit val rectangleArea: Area[Rectangle] = new Area[Rectangle] {
def area(a: Rectangle): Double = a.width * a.length
}
implicit val circleArea: Area[Circle] = new Area[Circle] {
def area(a: Circle): Double = Math.PI * (a.radius * a.radius)
}
}
这些实例也被称为 隐式值(implicit values),它们将成为 Rectangle
和 Circle
类型的候选类型类实例。
3.3. 接口对象(Interface Objects)
最简单的使用类型类的方式是将方法放在一个单例对象中:
object ShapeArea {
def areaOf[A](a: A)(implicit shape: Area[A]): Double = shape.area(a)
}
使用时,我们需要导入相关的类型类实例,然后调用对应的方法:
import AreaInstances._
ShapeArea.areaOf(rectangle)
编译器会自动搜索并插入合适的隐式参数:
ShapeArea.areaOf(rectangle)(rectangleArea)
3.4. 接口语法(Interface Syntax)
我们也可以使用扩展方法来为现有类型添加接口方法。Cats 中将这种方式称为 语法(syntax):
object ShapeAreaSyntax {
implicit class ShapeAreaOps[A](a: A) {
def areaOf(implicit shape: Area[A]): Double = shape.area(a)
}
}
使用时,需要同时导入实例和语法:
import AreaInstances._
import ShapeAreaSyntax._
Rectangle(2, 3).areaOf
同样,编译器会自动填充隐式参数:
Rectangle(2, 3).areaOf(rectangleArea)
4. 隐式机制(Implicits)
在 Scala 中使用类型类,意味着我们需要大量使用 隐式值(implicit values) 和 隐式参数(implicit parameters)。任何标记为 implicit
的定义都必须放在对象或 trait 内部,而不能放在顶层。
我们可以将类型类实例或隐式值打包到对象(如 AreaInstances
)、trait 或类型类的伴生对象中。
4.1. 隐式作用域(Implicit Scope)
编译器在调用方法时,如果发现缺少隐式参数,会自动在隐式作用域中搜索匹配的实例。例如:
ShapeArea.areaOf(Rectangle(2, 3))
编译器会寻找类型为 Area[Rectangle]
的隐式实例。如果发现多个候选实例,编译器会报错:ambiguous implicit values。Cats 利用这一机制实现了自动推导候选实例的功能。
5. Show 类型类
前面我们自己定义了类型类,而 Cats 提供了大量现成的类型类供我们使用。比如 Show[A]
,定义在 cats
包中:
package cats
trait Show[A] {
def show(value: A): String
}
5.1. 导入类型类
我们可以直接从 cats
包导入 Show
:
import cats.Show
每个类型类的伴生对象都提供了一个 apply
方法,用于获取指定类型的实例。
5.2. 导入默认实例
cats.instances
提供了常见类型的默认实例,如 Int
、String
、List
、Option
等:
import cats.instances.int._ // for Show
import cats.instances.string._ // for Show
val showInt: Show[Int] = Show.apply[Int]
val showString: Show[String] = Show.apply[String]
使用这些实例可以打印值:
val intAsString: String = showInt.show(123)
assert(showInt.show(123) == "123")
val stringAsString: String = showString.show("abc")
assert(showString.show("abc") == "abc")
5.3. 导入接口语法
通过导入 cats.syntax.show
,我们可以使用扩展方法:
import cats.syntax.show._ // for show
val shownInt = 123.show
assert(123.show == "123")
val shownString = "abc".show
assert("abc".show == "abc")
更推荐的方式是一次性导入所有实例和语法:
import cats.implicits._
5.4. 自定义实例
我们也可以为自定义类型实现 Show
实例:
object CustomInstance extends App {
implicit val customShow: Show[Date] =
new Show[Date] {
def show(date: Date): String =
s"${date.getTime}ms since the epoch."
}
}
val actualDate: String = new Date().show
val expectedDate: String = s"This year is: ${new Date().getYear}"
assert(actualDate == expectedDate)
Cats 还支持 SAM(Single Abstract Method)简化写法:
implicit val customShow: Show[Date] =
Show.show((date: Date) => s"${date.getTime}ms since the epoch.")
6. 半群(Semigroup)
Cats 提供了 Semigroup
类型类,用于聚合数据。它有一个 combine
方法,用于合并两个相同类型的值,遵循 结合律:
trait Semigroup[A] {
def combine(x: A, y: A): A
}
示例:
import cats.kernel.Semigroup
import cats.instances.int._
val onePlusTwo = Semigroup[Int].combine(1, 2)
结合律要求:
combine(a, combine(b, c)) = combine(combine(a, b), c)
结合律允许我们对数据进行任意分组,甚至并行处理。我们也可以自定义 Semigroup
实例,例如对 Int
使用乘法而不是加法:
implicit val multiplicationSemigroup = new Semigroup[Int] {
override def combine(x: Int, y: Int): Int = x * y
}
val four = Semigroup[Int].combine(2, 2)
或者更简洁的写法:
implicit val multiplicationSemigroup = Semigroup.instance[Int](_ * _)
对于集合类型,我们可以使用 fold
来操作:
def combineStrings(collection: Seq[String]): String = {
collection.foldLeft("")(Semigroup[String].combine)
}
⚠️ 但 Semigroup
本身不支持空集合的处理,因为没有默认值。这个问题可以通过 Monoid
来解决。
7. 幺半群(Monoid)
Monoid
是 Semigroup
的扩展,它添加了一个 单位元(empty),用于表示默认值:
trait Monoid[A] extends Semigroup[A] {
def empty: A
}
现在我们可以实现通用的 combineAll
方法:
def combineAll[A](collection: Seq[A])(implicit ev: Monoid[A]): A = {
val monoid = Monoid[A]
collection.foldLeft(monoid.empty)(monoid.combine)
}
单位元必须满足:
combine(x, empty) = combine(empty, x) = x
因此,单位元的选择依赖于上下文,而不仅仅是类型。这也是为什么 Monoid
和 Semigroup
的实现不仅依赖类型,还依赖 combine
操作。
8. 总结
本文介绍了 Cats 库的核心概念,包括类型类、隐式机制以及常用的类型类如 Show
、Semigroup
和 Monoid
。Cats 的设计非常模块化,提供了大量现成的工具供我们使用。
如需查看完整源码,请访问 GitHub 项目地址。