在 GitHub 上编辑此页面

实现类型类

类型类是一种抽象的、带参数的类型,它允许你向任何封闭的数据类型添加新行为,而无需使用子类型。这在多种用例中很有用,例如

  • 表达你所不拥有的类型(来自标准或第三方库)如何符合此类行为
  • 不涉及子类型关系(一个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的函数fF[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]

ListFunctor实例现在变为

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`

由于 MonadFunctor 的子类型,因此 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,它作用于函数,而不是像 ListOption 这样的数据类型。它可用于组合所有需要相同参数的多个函数。例如,需要访问某些配置、上下文、环境变量等的多个函数。

让我们定义一个 Config 类型和两个使用它的函数

trait Config
// ...
def compute(i: Int)(config: Config): String = ???
def show(str: String)(config: Config): Unit = ???

我们可能希望将 computeshow 合并到一个函数中,接受 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)相结合,允许对类型类进行简洁而自然的表达。