1. 概述
在本教程中,我们将学习如何在 Scala 中编写泛型且类型安全的代码。更重要的是,我们的库将能与那些在我们编写和编译库时还不知道的类型保持二进制兼容性。
因此,用户可以在不等待我们重新编译库的情况下,使用他们自定义的类型来调用我们库中的方法。同时,我们还可以做到在不使用继承或类型转换的前提下,编写出泛型且类型安全的代码。
这种 泛型编程 风格通常被称为 Ad-Hoc 多态(Ad-Hoc Polymorphism),而 Scala 对其提供了原生支持。Scala 社区广泛采用了一种基于 Ad-Hoc 多态的特定模式,即 类型类(Type Classes),这也是许多函数式编程库的基础。
2. 相关概念回顾
View Bounds 和 Context Bounds 实际上是语法糖,编译器在早期编译阶段会将它们转换为更基础的语言结构。在本节中,我们将回顾这些基础特性。
在 Scala 中,我们可以定义 具有多个参数列表的方法和构造函数。这是函数式编程特性(如 柯里化和部分应用)的基础。
我们也可以使用它们来编写在调用时看起来像语言控制结构的代码。在下面的例子中,我们创建了一个有两个参数列表的方法,并且第二个参数列表只包含一个参数,这允许我们使用大括号而不是小括号来调用它:
object multipleparameterlists {
val amIcool: Boolean = true
def cond(pred: => Boolean)(proc: => Unit): Unit = {
if (pred) proc else ()
}
cond(amIcool) {
println("You are cool")
}
}
此外,我们可以将其中一个参数列表标记为 implicit。
这会指示编译器在方法调用时自动补全该参数列表。如果调用时未显式提供该参数列表的值,编译器会在当前作用域中查找标记为 implicit 且类型匹配的变量:
object implicitvalues {
def printDebugMsg(msg: String)(implicit debugging: Boolean): Unit = {
if (debugging) println(msg) else ()
}
implicit val debugging: Boolean = true
printDebugMsg("I am debugging this method")
}
在最后一行中,我们只显式传入了第一个参数列表。编译器通过查找作用域内的 implicit 变量自动补全了第二个参数列表。在这个例子中,这个变量就是 debugging
。
2.1. 使用隐式参数实现多态行为
在静态类型语言中编写通用的多态代码可能比较困难。但我们要努力做到 不依赖运行时反射或类型转换这类“后门”。
我们可以通过添加约束来使用我们的代码;例如,我们可以要求调用者提供一个特定类型的函数:
object conversion {
abstract class Order[T](val me: T) {
def less(other: T): Boolean
}
val intToOrder: Int => Order[Int] = x => new Order[Int](x) {
override def less(other: Int): Boolean = me < other
}
def maximum[A](a: A, b: A)(toOrder: A => Order[A]): A = {
if (toOrder(a).less(b)) b else a
}
val a = 5
val b = 9
println(s"The maximum($a, $b) is ${maximum(a, b)(intToOrder)}")
}
不幸的是,由于我们添加了约束,使用这段代码会变得非常啰嗦,因为用户必须在每次调用时显式传递这个函数。
我们可以通过将约束参数列表标记为 implicit 来简化调用语法:
object implicitconversion {
abstract class Order[T](val me: T) {
def less(other: T): Boolean
}
implicit val intToOrder: Int => Order[Int] = x => new Order[Int](x) {
override def less(other: Int): Boolean = me < other
}
def maximum[A](a: A, b: A)(implicit toOrder: A => Order[A]): A = {
if (toOrder(a).less(b)) b else a
}
val a = 5
val b = 9
println(s"The maximum($a, $b) is ${maximum(a, b)}")
}
可以看到,我们的函数 maximum
现在变得更容易使用了,因为我们可以不显式传递约束函数。
类型约束为编译器提供了关于我们的库所接受类型应支持哪些操作的上下文信息。这使我们能够在不依赖运行时技巧(如反射或类型转换)的情况下编写更广泛的应用程序。
Scala 提供了两种类型的语法支持:一种是要求隐式类型转换,另一种是要求实现特定复合类型的模块。
3. View Bounds
首先,我们来讨论如何在方法中使用 隐式转换 作为类型约束。约束是一个类型转换,我们可以将其理解为“类型 A 应该可以被看作类型 B”。这也限制了可以接受的类型,因此被称为 View Bound。
使用 “<%”
类型操作符,我们可以简化前面示例中的代码:
object viewbound {
abstract class Order[T](val me: T) {
def less(other: T): Boolean
}
implicit val intToOrder: Int => Order[Int] = x => new Order[Int](x) {
override def less(other: Int): Boolean = me < other
}
def maximum[A <% Order[A]](a: A, b: A): A = {
val toOrder = implicitly[A => Order[A]]
if (toOrder(a).less(b)) b else a
}
val a = 5
val b = 9
println(s"The maximum($a, $b) is ${maximum(a, b)}")
}
3.1. 关于 View Bounds 的注意事项
隐式转换 是一个颇具争议的特性,现在强烈不推荐使用。
首先,Scala 已经将隐式转换隐藏在语言特性后面。要使用它,我们需要通过添加编译器标志或导入特性来显式激活。最重要的是,语言维护者将来可能会完全弃用它。
4. Context Bounds
Scala 受到了 Haskell 的影响,Type Classes 的概念就是从那里来的。与要求类型转换不同,我们要求的是一个实现了特定函数集合的模块。
Scala 在运行时将函数分组到对象中。因此,我们可以要求存在一个给定的上下文对象,而不是分别声明每个约束函数:
object implicitobject {
abstract class Order[T] {
def less(me: T, other: T): Boolean
}
implicit val intOrder: Order[Int] = new Order[Int] {
override def less(me: Int, other: Int): Boolean = me < other
}
def maximum[A](a: A, b: A)(implicit ord: Order[A]): A = {
if (ord.less(a, b)) b else a
}
val a = 5
val b = 9
println(s"The maximum($a, $b) is ${maximum(a, b)}")
}
与 Context Bounds 类似,语言提供了语法糖来减少声明和使用的啰嗦程度:
def maximum[A : Order](a: A, b: A)(implicit ord: Order[A]): A
4.1. 使用上下文对象
Context Bound 语法简洁易用,但其“证据”没有名字,我们只能将其转发给其他方法。要直接使用它,我们可以选择使用更冗长的语法,或者 使用 Context Bound 特性,然后使用 implicitly
获取证据的引用:
object contextbound {
abstract class Order[T] {
def less(me: T, other: T): Boolean
}
implicit val intOrder: Order[Int] = new Order[Int] {
override def less(me: Int, other: Int): Boolean = me < other
}
def maximum[A: Order](a: A, b: A): A = {
val ord = implicitly[Order[A]]
if (ord.less(a, b)) b else a
}
val a = 5
val b = 9
println(s"The maximum($a, $b) is ${maximum(a, b)}")
}
在这个例子中,我们使用 implicitly
关键字引用了上下文对象,并给它起了一个名字。这样我们就可以直接使用它,或将它转发给其他方法。
4.2. Context Bounds 是否和 View Bounds 一样强大?
大多数 View Bound 的使用场景是通过转换来实现 Rich Object 模式;我们可以通过在 Context Bound 中传递 Rich Interface 隐式类来实现这一点。
不幸的是,我们失去了一些功能,比如直接调用扩展方法,但 我们可以通过在上下文对象中引入 Implicit Class 来找回这些功能:
object richobject {
abstract class Order[T] {
def less(me: T, other: T): Boolean
implicit class RichInterface(val me: T) {
def <(other: T): Boolean = less(me, other)
}
}
implicit val intOrder: Order[Int] = new Order[Int] {
override def less(me: Int, other: Int): Boolean = me < other
}
def maximum[A: Order](a: A, b: A): A = {
val ord = implicitly[Order[A]]
import ord._
if (a < b) b else a
}
val a = 5
val b = 9
println(s"The maximum($a, $b) is ${maximum(a, b)}")
}
通过在上下文对象中引入隐式类,我们的方法用户可以在相关作用域中导入它,就像上面的例子中那样。
这个例子表明,Context Bounds 和 View Bounds 在表达能力上是等价的,也说明了 View Bounds 的弃用并不会削弱 Scala 的表达能力。
5. 总结
在本教程中,我们探讨了如何使用隐式参数编写灵活的泛型代码,并介绍了两种不同的模式:View Bounds 和 Context Bounds。
我们还理解了 为什么要避免使用 View Bounds,更重要的是,我们学会了如何使用更安全的 Context Bound 方法编写等效代码。