实现类型类
类型类是一种抽象的、带参数的类型,它允许你向任何封闭的数据类型添加新行为,而无需使用子类型。这在多种用例中很有用,例如
- 表达你所不拥有的类型(来自标准或第三方库)如何符合此类行为
- 不涉及子类型关系(一个
extends
另一个)的情况下,为多种类型表达此类行为(例如:即席多态性)
因此,在 Scala 3 中,类型类只是具有一个或多个参数的特征,其实现不是通过extends
关键字定义的,而是通过给定实例。以下是常见类型类的示例
半群和幺半群
以下是Monoid
类型类定义
trait SemiGroup[T]:
extension (x: T) def combine (y: T): T
trait Monoid[T] extends SemiGroup[T]:
def unit: T
String
类型的此Monoid
类型类的实现可以如下所示
given Monoid[String] with
extension (x: String) def combine (y: String): String = x.concat(y)
def unit: String = ""
而对于Int
类型,可以编写以下内容
given Monoid[Int] with
extension (x: Int) def combine (y: Int): Int = x + y
def unit: Int = 0
此幺半群现在可以用作以下combineAll
方法中的上下文绑定
def combineAll[T: Monoid](xs: List[T]): T =
xs.foldLeft(summon[Monoid[T]].unit)(_.combine(_))
为了摆脱summon[...]
,我们可以按如下方式定义一个Monoid
对象
object Monoid:
def apply[T](using m: Monoid[T]) = m
这将允许以这种方式重新编写combineAll
方法
def combineAll[T: Monoid](xs: List[T]): T =
xs.foldLeft(Monoid[T].unit)(_.combine(_))
函子
类型的Functor
提供了对其值进行“映射”的能力,即应用一个函数,该函数在值内部进行转换,同时记住其形状。例如,修改集合的每个元素,而不删除或添加元素。我们可以用F
表示所有可以“映射”的类型。它是一个类型构造器:当提供类型参数时,其值类型变为具体类型。因此,我们写成F[_]
,暗示类型F
将另一种类型作为参数。泛型Functor
的定义将写成
trait Functor[F[_]]:
def map[A, B](x: F[A], f: A => B): F[B]
可以这样解读:“类型构造器F[_]
的Functor
表示通过应用类型为A => B
的函数f
将F[A]
转换为F[B]
的能力”。我们在此处将Functor
定义称为类型类。这样,我们可以为List
类型定义Functor
的实例
given Functor[List] with
def map[A, B](x: List[A], f: A => B): List[B] =
x.map(f) // List already has a `map` method
在此given
实例的范围内,在任何需要具有Functor
上下文绑定的类型的任何位置,编译器都将接受使用List
。
例如,我们可以编写这样的测试方法
def assertTransformation[F[_]: Functor, A, B](expected: F[B], original: F[A], mapping: A => B): Unit =
assert(expected == summon[Functor[F]].map(original, mapping))
并以这种方式使用它,例如
assertTransformation(List("a1", "b1"), List("a", "b"), elt => s"${elt}1")
这是第一步,但实际上我们可能希望map
函数是直接在类型F
上可访问的方法。这样,我们就可以直接在F
的实例上调用map
,并摆脱summon[Functor[F]]
部分。与前面的幺半群示例一样,extension
方法有助于实现这一点。让我们使用扩展方法重新定义Functor
类型类。
trait Functor[F[_]]:
extension [A](x: F[A])
def map[B](f: A => B): F[B]
List
的Functor
实例现在变为
given Functor[List] with
extension [A](xs: List[A])
def map[B](f: A => B): List[B] =
xs.map(f) // List already has a `map` method
它简化了assertTransformation
方法
def assertTransformation[F[_]: Functor, A, B](expected: F[B], original: F[A], mapping: A => B): Unit =
assert(expected == original.map(mapping))
map
方法现在直接用于original
。它可用作扩展方法,因为original
的类型是F[A]
,并且给定实例Functor[F[A]]
(定义map
)在范围内。
单子
将 map
应用于 Functor[List]
中的类型为 A => B
的映射函数会导致 List[B]
。因此,将其应用于类型为 A => List[B]
的映射函数会导致 List[List[B]]
。为了避免管理列表列表,我们可能希望在单个列表中“展平”值。
这就是 Monad
的用武之地。类型 F[_]
的 Monad
是具有另外两个操作的 Functor[F]
flatMap
,当给定类型为A => F[B]
的函数时,它将F[A]
转换为F[B]
,pure
,它从单个值A
创建F[A]
。
以下是此定义在 Scala 3 中的翻译
trait Monad[F[_]] extends Functor[F]:
/** The unit value for a monad */
def pure[A](x: A): F[A]
extension [A](x: F[A])
/** The fundamental composition operation */
def flatMap[B](f: A => F[B]): F[B]
/** The `map` operation can now be defined in terms of `flatMap` */
def map[B](f: A => B) = x.flatMap(f.andThen(pure))
end Monad
列表
可以通过此 given
实例将 List
转换为单子
given listMonad: Monad[List] with
def pure[A](x: A): List[A] =
List(x)
extension [A](xs: List[A])
def flatMap[B](f: A => List[B]): List[B] =
xs.flatMap(f) // rely on the existing `flatMap` method of `List`
由于 Monad
是 Functor
的子类型,因此 List
也是一个函子。函子的 map
操作已由 Monad
特征提供,因此该实例不需要显式定义它。
选项
Option
是具有相同行为的另一种类型
given optionMonad: Monad[Option] with
def pure[A](x: A): Option[A] =
Option(x)
extension [A](xo: Option[A])
def flatMap[B](f: A => Option[B]): Option[B] = xo match
case Some(x) => f(x)
case None => None
读取器
Monad
的另一个示例是 Reader Monad,它作用于函数,而不是像 List
或 Option
这样的数据类型。它可用于组合所有需要相同参数的多个函数。例如,需要访问某些配置、上下文、环境变量等的多个函数。
让我们定义一个 Config
类型和两个使用它的函数
trait Config
// ...
def compute(i: Int)(config: Config): String = ???
def show(str: String)(config: Config): Unit = ???
我们可能希望将 compute
和 show
合并到一个函数中,接受 Config
作为参数,并显示计算结果,并且我们希望使用单子来避免多次显式传递参数。因此,假设正确的 flatMap
操作,我们可以编写
def computeAndShow(i: Int): Config => Unit = compute(i).flatMap(show)
而不是
show(compute(i)(config))(config)
然后让我们定义此 flatMap
。首先,我们将定义一个名为 ConfigDependent
的类型,该类型表示一个函数,当传递 Config
时会生成 Result
。
type ConfigDependent[Result] = Config => Result
单子实例将如下所示
given configDependentMonad: Monad[ConfigDependent] with
def pure[A](x: A): ConfigDependent[A] =
config => x
extension [A](x: ConfigDependent[A])
def flatMap[B](f: A => ConfigDependent[B]): ConfigDependent[B] =
config => f(x(config))(config)
end configDependentMonad
可以使用 类型 lambda 编写 ConfigDependent
类型
type ConfigDependent = [Result] =>> Config => Result
使用此语法会将之前的 configDependentMonad
变成
given configDependentMonad: Monad[[Result] =>> Config => Result] with
def pure[A](x: A): Config => A =
config => x
extension [A](x: Config => A)
def flatMap[B](f: A => Config => B): Config => B =
config => f(x(config))(config)
end configDependentMonad
我们很可能希望将此模式与我们 Config
特征之外的其他类型环境一起使用。Reader 单子允许我们抽象出 Config
作为类型参数,在以下定义中命名为 Ctx
given readerMonad[Ctx]: Monad[[X] =>> Ctx => X] with
def pure[A](x: A): Ctx => A =
ctx => x
extension [A](x: Ctx => A)
def flatMap[B](f: A => Ctx => B): Ctx => B =
ctx => f(x(ctx))(ctx)
end readerMonad
摘要
类型类的定义用具有抽象成员的参数化类型表示,例如 trait
。子类型多态性和使用类型类进行即席多态性之间的主要区别在于类型类的定义是如何实现的,相对于它作用的类型。在类型类的情况下,它对具体类型的实现通过 given
实例定义表示,该定义作为隐式参数与它作用的值一起提供。使用子类型多态性,实现将混合到类的父级中,并且只需要一个术语来执行多态操作。类型类解决方案需要更多的设置工作,但具有更好的可扩展性:向类添加新接口需要更改该类的源代码。但相反,类型类的实例可以在任何地方定义。
总之,我们已经看到,特征和给定实例与其他构造(如扩展方法、上下文边界和类型 lambda)相结合,允许对类型类进行简洁而自然的表达。