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)
中的 a
和 b
)和类型参数(如参数化多态中所见),还可以接收 隐式参数,即使用 implicit
关键字标记的参数列表。
以 Scala 集合中的 sorted
方法为例:
def sorted[B >: A](implicit ord: Ordering[B]): Repr
这个方法接收一个泛型类型 B
,使得它可以适用于几乎任何类型。但它还需要一个 Ordering[B]
类型的参数 ord
,用于定义该类型的排序策略。
Scala 中的原生类型(如 Int
、String
等)都有默认的 Ordering
实现。如果你定义了一个自定义类型,并希望它是可排序的,则必须显式提供一个 Ordering
实例。
⚠️ 关键在于 implicit
关键字:如果开发者没有显式传入 ord
参数,编译器会自动尝试在作用域内查找一个匹配的隐式值,并自动传入。
换句话说,编译器会尝试隐式地解决依赖,而不是强制开发者显式提供。
如果编译器在所有可用作用域中都找不到匹配的隐式值,它就会报错。
2.2. 隐式参数缺失错误
我们可以直接对 Int
、String
等内置类型进行排序,而无需额外定义 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"
的类型是 String
,34
的类型是 Int
。
因此,String
和 Int
是 具体类型(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
实例。
像 List
、Option
、Either
、Future
这样的容器类型,在没有指定泛型参数前,都是抽象类型。
容器类型让我们可以在值或类型上进行抽象,这也是我们可以写出如下代码的原因:
def sort[A](xs: List[A]): List[A]
这就是我们之前提到的 参数化多态(parametric polymorphism)。我们可以对任意类型的 List
使用 sort
方法,只需将类型参数 A
替换为具体类型(如 String
或 Int
)。
✅ 总结:
- 具体值 → 具体类型(proper types)
- 容器类型(如
List
) → 类型构造器(type constructors)
3.5. 在类型构造器上抽象(Abstracting Over Type Constructors)
我们已经可以在具体类型上进行抽象(如 List[A]
),那么能不能在类型构造器本身上进行抽象呢?
比如,我们想定义一个函数,可以处理 List
或 Option
等任意容器类型:
def doubleIt[F[Int]](xs: F[Int]): F[Int]
这类似于参数化多态,但这次是作用在类型构造器上的。我们希望这个函数能适用于任意容器类型,而不需要为每种容器类型都写一个实现。
3.6. 高阶类型(Higher Kinds)
我们可以使用 类型类(type class) 来实现这一点。
doubleIt
需要知道如何处理任意 F[Int]
类型,无论是 List
、Option
还是其他容器类型。
我们可以通过隐式参数来提供帮助:
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
就能自动处理 List
和 Option
中的整数了:
"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 获取。