1. 引言

在这篇文章中,我们将深入探讨 Scalaz 的核心设计理念。Scalaz 是 Scala 生态系统中非常常见且强大的库之一,它将函数式编程的最佳实践引入 Scala 世界,包括纯函数和不可变数据结构。

✅ Scalaz 不仅扩展了现有的 Scala 特性,使其更具表达力和简洁性,还提供了一整套工具方法,让我们可以用纯函数式的方式完成几乎任何操作。

如果你是 Scala 新手,可以先参考我们的入门文章:《Scala 入门指南》。为了更好地理解本文内容,建议你对 多态(polymorphism) 有一定了解。


2. 隐式机制(Implicits)

Scalaz 高度依赖于 ad-hoc 多态(ad-hoc polymorphism)。而要理解 Scala 是如何支持 ad-hoc 多态的,就必须先掌握 隐式机制(implicits) 这一核心概念。

⚠️ 隐式机制是许多从其他语言转到 Scala 的开发者最容易困惑、踩坑最多的地方之一。

2.1. 隐式参数(Implicit Parameters)

Scala 中的方法除了可以接收普通参数(如 def max(a: Int, b: Int) 中的 ab)和类型参数(如参数化多态中所见),还可以接收 隐式参数,即使用 implicit 关键字标记的参数列表。

以 Scala 集合中的 sorted 方法为例:

def sorted[B >: A](implicit ord: Ordering[B]): Repr

这个方法接收一个泛型类型 B,使得它可以适用于几乎任何类型。但它还需要一个 Ordering[B] 类型的参数 ord,用于定义该类型的排序策略。

Scala 中的原生类型(如 IntString 等)都有默认的 Ordering 实现。如果你定义了一个自定义类型,并希望它是可排序的,则必须显式提供一个 Ordering 实例。

⚠️ 关键在于 implicit 关键字:如果开发者没有显式传入 ord 参数,编译器会自动尝试在作用域内查找一个匹配的隐式值,并自动传入。

换句话说,编译器会尝试隐式地解决依赖,而不是强制开发者显式提供。

如果编译器在所有可用作用域中都找不到匹配的隐式值,它就会报错。


2.2. 隐式参数缺失错误

我们可以直接对 IntString 等内置类型进行排序,而无需额外定义 Ordering

it should "sort ints and strings" in {
    val integers = List(3, 2, 6, 5, 4, 1)
    val strings = List("c", "b", "f", "e", "d", "a")

    assertResult(expected = List(1, 2, 3, 4, 5, 6))(integers.sorted)
    assertResult(expected = List("a", "b", "c", "d", "e", "f"))(strings.sorted)
}

假设我们定义了一个新的类型:

case class IntWrapper(id: Int)

然后尝试对 List[IntWrapper] 调用 sorted 方法:

it should "sort custom types" in {
    val wrappedInts = List(IntWrapper(3), IntWrapper(2), IntWrapper(1))

    assertResult(expected = List(IntWrapper(1), IntWrapper(2), IntWrapper(3)))(wrappedInts.sorted)
}

此时编译会失败,并提示:

No implicit arguments of type: Ordering[IntWrapper]

这个错误明确告诉我们:Scala 不知道如何对 IntWrapper 类型进行排序,我们需要为它提供一个 Ordering 实现。


2.3. 隐式值(Implicit Values)

虽然我们可以在调用方法时显式传入隐式参数,但这违背了隐式机制的初衷。

我们可以使用 implicit 关键字定义一个隐式值,这样就不需要手动传递了:

implicit val ord: Ordering[IntWrapper] = (x, y) => x.id.compareTo(y.id)

只要这个隐式值在作用域内,之前的错误就会消失,编译器会自动使用这个排序策略。隐式值不需要定义在同一个方法中,它可以在整个类作用域内,甚至通过 import 引入。

通过这种方式,我们可以让 List.sorted 方法适用于任意类型,只需为每个类型提供一个对应的 Ordering 实现,编译器会自动切换使用。


3. 高阶类型(Higher-Kinded Types)

函数式编程中,所有的变量和值都基于某种类型体系构建。这是 Scala 和函数式编程中的一个进阶话题。

我们从类型的基础开始,逐步构建类型体系。

3.1. 具体值(Proper Values)

看下面的变量声明:

val name = "Greg" 
val age = 34

字符串 "Greg" 和数字 34 都是具体值,它们不需要进一步解释就能完整表达含义。

我们称这类值为 具体值(proper values),因为它们已经处于最终状态。


3.2. 具体类型(Proper Types)

Scala 编译器能自动推断出 "Greg" 的类型是 String34 的类型是 Int

因此,StringInt具体类型(proper types),它们在类型系统中已经处于最终状态,只需要包含具体数据即可。

我们也可以显式声明类型:

val name: String = "Greg"

3.3. 抽象类型(Abstract Types)

再看一个例子:

val fruits = List("Oranges", "Apples", "Mangoes")

变量 fruits 的值是具体值,编译器能推断其类型为 List[String]

但如果显式声明为:

val fruits: List = List("Oranges", "Apples", "Mangoes")

编译会失败,提示:Type List takes type parameters

因为 List 并不是一个完整类型,它需要一个泛型参数才能成为具体类型。我们称 List抽象类型(abstract type),因为它在类型体系中处于更高的层级。

只有当 List[String] 这样使用时,它才成为一个具体类型。


3.4. 类型构造器(Type Constructors)

List[_] 是一个 类型构造器(type constructor)。给它一个具体类型(如 String),它就能构造出 List[String]

这类似于值构造器 StudentId(_),给它一个整数 ID,它就能构造出 StudentId 实例。

ListOptionEitherFuture 这样的容器类型,在没有指定泛型参数前,都是抽象类型。

容器类型让我们可以在值或类型上进行抽象,这也是我们可以写出如下代码的原因:

def sort[A](xs: List[A]): List[A]

这就是我们之前提到的 参数化多态(parametric polymorphism)。我们可以对任意类型的 List 使用 sort 方法,只需将类型参数 A 替换为具体类型(如 StringInt)。

✅ 总结:

  • 具体值 → 具体类型(proper types)
  • 容器类型(如 List) → 类型构造器(type constructors)

3.5. 在类型构造器上抽象(Abstracting Over Type Constructors)

我们已经可以在具体类型上进行抽象(如 List[A]),那么能不能在类型构造器本身上进行抽象呢?

比如,我们想定义一个函数,可以处理 ListOption 等任意容器类型:

def doubleIt[F[Int]](xs: F[Int]): F[Int]

这类似于参数化多态,但这次是作用在类型构造器上的。我们希望这个函数能适用于任意容器类型,而不需要为每种容器类型都写一个实现。


3.6. 高阶类型(Higher Kinds)

我们可以使用 类型类(type class) 来实现这一点。

doubleIt 需要知道如何处理任意 F[Int] 类型,无论是 ListOption 还是其他容器类型。

我们可以通过隐式参数来提供帮助:

def doubleIt[F[Int]](xs: F[Int])(implicit doubler: Doubler[F]): F[Int] = {
    doubler.makeDouble(xs)
}

接下来定义类型类:

trait Doubler[F[Int]] {
    def makeDouble(xs: F[Int]): F[Int]
}

object Doubler {
    implicit object listDoubler extends Doubler[List] {
        def makeDouble(xs: List[Int]): List[Int] = xs.map(_ * 2)
    }
    implicit object optionDoubler extends Doubler[Option] {
        def makeDouble(xs: Option[Int]): Option[Int] = xs.map(_ * 2)
    }
}

现在,doubleIt 就能自动处理 ListOption 中的整数了:

"Doubler" should "work on any supported container type" in {
    val list: List[Int] = List(1,2,3)
    val opt: Option[Int] = Some(5)

    assertResult(expected = List(2,4,6))(actual = doubleIt(list))
    assertResult(expected = Some(10))(actual = doubleIt(opt))
}

这就是 高阶多态(higher-kinded polymorphism) 的由来:在类型构造器上进行抽象

通常我们会看到类似 F[_] 的写法,而不是像 F[Int] 这样的具体类型。

如果我们还想让 doubleIt 支持任意类型(如 List[String]),则需要进一步泛化方法签名:

def doubleIt[F[_], A](xs: F[A])(implicit doubler: Doubler[F,A]): F[A]

同时更新类型类:

trait Doubler[F[_], A] {
    def makeDouble(xs: F[A]): F[A]
}

object Doubler {
    ...
    implicit object stringListDoubler extends Doubler[List, String] {
        def makeDouble(xs: List[String]): List[String] = xs.map(s => s concat s)
    }
    ...
}

4. 总结

在这篇文章中,我们介绍了构成 Scalaz 核心的几个 基础概念

  • 隐式机制(Implicits)
  • 高阶类型(Higher-Kinded Types)
  • 类型类(Type Classes)
  • Pimp My Library 模式

当你在使用 Scalaz 时,这些概念会反复出现。理解它们将帮助你更深入地掌握 Scalaz 的设计思想和使用方式。

你可以通过以下链接继续深入学习:

完整代码可在 GitHub 获取。


原始标题:Threads vs Coroutines in Kotlin