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),它们将成为 RectangleCircle 类型的候选类型类实例。

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 提供了常见类型的默认实例,如 IntStringListOption 等:

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)

MonoidSemigroup 的扩展,它添加了一个 单位元(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

因此,单位元的选择依赖于上下文,而不仅仅是类型。这也是为什么 MonoidSemigroup 的实现不仅依赖类型,还依赖 combine 操作。

8. 总结

本文介绍了 Cats 库的核心概念,包括类型类、隐式机制以及常用的类型类如 ShowSemigroupMonoid。Cats 的设计非常模块化,提供了大量现成的工具供我们使用。

如需查看完整源码,请访问 GitHub 项目地址


原始标题:Overview of Kotlin Collections API