在 GitHub 上编辑此页面

类型类派生

类型类派生是一种为满足某些简单条件的类型类自动生成给定实例的方法。此意义上的类型类是具有单个类型参数(用于确定正在操作的类型)的任何特征或类,以及特殊情况CanEqual。常见的示例是EqOrderingShow。例如,给定以下Tree代数数据类型 (ADT)

enum Tree[T] derives Eq, Ordering, Show:
  case Branch(left: Tree[T], right: Tree[T])
  case Leaf(elem: T)

derives子句在Tree的伴生对象中为EqOrderingShow类型类生成以下给定实例

given [T: Eq]       : Eq[Tree[T]]       = Eq.derived
given [T: Ordering] : Ordering[Tree[T]] = Ordering.derived
given [T: Show]     : Show[Tree[T]]     = Show.derived

我们说Tree派生类型,而EqOrderingShow实例是派生实例

注意:可以手动使用derived,当您无法控制定义时,这很有用。例如,我们可以像这样为Option实现Ordering

given [T: Ordering]: Ordering[Option[T]] = Ordering.derived

如果您能使用 derives 子句,则不建议直接引用 derived 成员。

所有数据类型都可以有 derives 子句。本文主要关注那些也有 Mirror 类型类的给定实例可用数据类型。

确切机制

在以下情况下,当类型参数被枚举并且第一个索引计算的值大于最后一个索引时,则实际上没有参数,例如:A[T_2, ..., T_1] 表示 A

对于类/特征/对象/枚举 DerivingType[T_1, ..., T_N] derives TC,在 DerivingType 的伴随对象中创建派生实例(如果它是对象,则为 DerivingType 本身)。

派生实例的一般“形状”如下

given [...](using ...): TC[ ... DerivingType[...] ... ] = TC.derived

TC.derived 应为符合左侧预期类型的表达式,可能会使用术语和/或类型推理进行详细说明。

注意: TC.derived 是正常访问,因此如果有多个 TC.derived 定义,则应用重载解析。

派生实例的具体外观取决于 DerivingTypeTC 的具体情况,我们首先检查 TC

TC 接受 1 个参数 F

因此,TC 被定义为 TC[F[A_1, ..., A_K]](如果 K == 0,则为 TC[F]),其中 F 是某些内容。根据参数的种类,还有两种进一步的情况

FDerivingType 的所有参数的种类为 *

注意: 在这种情况下,K == 0

然后生成的实例为

given [T_1: TC, ..., T_N: TC]: TC[DerivingType[T_1, ..., T_N]] = TC.derived

这是最常见的情况,也是引言中强调的情况。

注意: [T_i: TC, ...] 引入了 (using TC[T_i], ...),有关详细信息,请参阅 上下文界限。这允许 derived 成员访问这些证据。

注意: 如果 N == 0,则上述表示

given TC[DerivingType] = TC.derived

例如,类

case class Point(x: Int, y: Int) derives Ordering

生成实例

object Point:
  ...
  given Ordering[Point] = Ordering.derived

FDerivingType 在右侧具有匹配种类的参数

本节涵盖了您可以从右侧开始配对 FDerivingType 的参数的情况,使得它们成对具有相同的种类,并且 FDerivingType(或两者)的所有参数都用完了。F 还必须至少有一个参数。

然后,一般形状将是

given [...]: TC[ [...] =>> DerivingType[...] ] = TC.derived

当然,TCDerivingType 应用于正确类型的类型。

为了实现此功能,我们将它分成 3 个案例

如果 FDerivingType 采用相同数量的参数(N == K

given TC[DerivingType] = TC.derived
// simplified form of:
given TC[ [A_1, ..., A_K] =>> DerivingType[A_1, ..., A_K] ] = TC.derived

如果 DerivingType 采用的参数少于 FN < K),我们仅使用类型 lambda 中最右侧的参数

given TC[ [A_1, ..., A_K] =>> DerivingType[A_(K-N+1), ..., A_K] ] = TC.derived

// if DerivingType takes no arguments (N == 0), the above simplifies to:
given TC[ [A_1, ..., A_K] =>> DerivingType ] = TC.derived

如果 F 采用的参数少于 DerivingTypeK < N),我们将剩余的最左侧插槽填入给定类型的类型参数

given [T_1, ... T_(N-K)]: TC[[A_1, ..., A_K] =>> DerivingType[T_1, ... T_(N-K), A_1, ..., A_K]] = TC.derived

TCCanEqual 类型类

因此,我们有:DerivingType[T_1, ..., T_N] 导出 CanEqual

U_1, ..., U_MDerivingType 的类型为 * 的参数。(这些是 T_i 的子集)

然后生成的实例为

given [T_1L, T_1R, ..., T_NL, T_NR]                            // every parameter of DerivingType twice
      (using CanEqual[U_1L, U_1R], ..., CanEqual[U_ML, U_MR]): // only parameters of DerivingType with kind *
        CanEqual[DerivingType[T_1L, ..., T_NL], DerivingType[T_1R, ..., T_NR]] = // again, every parameter
          CanEqual.derived

T_i 的界限得到正确处理,例如:T_2 <: T_1 变为 T_2L <: T_1L

例如,类

class MyClass[A, G[_]](a: A, b: G[B]) derives CanEqual

生成以下给定实例

object MyClass:
  ...
  given [A_L, A_R, G_L[_], G_R[_]](using CanEqual[A_L, A_R]): CanEqual[MyClass[A_L, G_L], MyClass[A_R, G_R]] = CanEqual.derived

TC 不适用于自动派生

抛出错误。

具体错误取决于上述哪个条件失败。例如,如果 TC 采用超过 1 个参数并且不是 CanEqual,则错误为 DerivingType 无法与 TC 的类型参数统一

所有数据类型都可以有 derives 子句。本文档的其余部分主要关注同时具有 Mirror 类型类的给定实例的数据类型。

Mirror

scala.deriving.Mirror 类型类实例在类型级别提供有关类型组件和标记的信息。它们还提供最小的术语级别基础设施,以允许更高级别的库提供全面的派生支持。

Mirror 类型类的实例由编译器无条件地自动生成,适用于

  • 枚举和枚举案例,
  • 案例对象。

Mirror 的实例也有条件地生成,适用于

  • 在调用位置可见构造函数的 case 类(如果伴随对象不是 case 对象,则始终为真)
  • 密封类和密封特征,其中
    • 存在至少一个子 case
    • 每个子 case 都可以从父定义中访问
    • 如果 sealed trait/class 没有伴生对象,则每个子 case 都可以通过镜像类型的 prefix 从调用点访问
    • 并且编译器可以为每个子 case 生成一个 Mirror 类型类实例

scala.deriving.Mirror 类型类的定义如下

sealed trait Mirror:

  /** the type being mirrored */
  type MirroredType

  /** the type of the elements of the mirrored type */
  type MirroredElemTypes

  /** The mirrored *-type */
  type MirroredMonoType

  /** The name of the type */
  type MirroredLabel <: String

  /** The names of the elements of the type */
  type MirroredElemLabels <: Tuple

object Mirror:

  /** The Mirror for a product type */
  trait Product extends Mirror:

    /** Create a new instance of type `T` with elements
     *  taken from product `p`.
     */
    def fromProduct(p: scala.Product): MirroredMonoType

  trait Sum extends Mirror:

    /** The ordinal number of the case class of `x`.
     *  For enums, `ordinal(x) == x.ordinal`
     */
    def ordinal(x: MirroredMonoType): Int

end Mirror

乘积类型(即 case 类和对象,以及枚举 case)的镜像是 Mirror.Product 的子类型。和类型(即具有乘积子类型的 sealed class 或 trait,以及枚举)的镜像是 Mirror.Sum 的子类型。

对于上面的 Tree ADT,编译器将自动提供以下 Mirror 实例

// Mirror for Tree
new Mirror.Sum:
  type MirroredType = Tree
  type MirroredElemTypes[T] = (Branch[T], Leaf[T])
  type MirroredMonoType = Tree[_]
  type MirroredLabel = "Tree"
  type MirroredElemLabels = ("Branch", "Leaf")

  def ordinal(x: MirroredMonoType): Int = x match
    case _: Branch[_] => 0
    case _: Leaf[_] => 1

// Mirror for Branch
new Mirror.Product:
  type MirroredType = Branch
  type MirroredElemTypes[T] = (Tree[T], Tree[T])
  type MirroredMonoType = Branch[_]
  type MirroredLabel = "Branch"
  type MirroredElemLabels = ("left", "right")

  def fromProduct(p: Product): MirroredMonoType =
    new Branch(...)

// Mirror for Leaf
new Mirror.Product:
  type MirroredType = Leaf
  type MirroredElemTypes[T] = Tuple1[T]
  type MirroredMonoType = Leaf[_]
  type MirroredLabel = "Leaf"
  type MirroredElemLabels = Tuple1["elem"]

  def fromProduct(p: Product): MirroredMonoType =
    new Leaf(...)

如果无法为给定类型自动生成 Mirror,则会出现一个错误,解释为什么它既不是受支持的和类型也不是乘积类型。例如,如果 A 是一个未 sealed 的 trait

No given instance of type deriving.Mirror.Of[A] was found for parameter x of method summon in object Predef. Failed to synthesize an instance of type deriving.Mirror.Of[A]:
     * trait A is not a generic product because it is not a case class
     * trait A is not a generic sum because it is not a sealed trait

注意 Mirror 类型的以下属性

  • 属性使用类型而不是术语进行编码。这意味着它们在未使用时没有运行时占用空间,并且它们是 Scala 3 元编程设施的编译时功能。
  • 镜像类型是局部类或内部类没有限制。
  • MirroredTypeMirroredElemTypes 的类型与镜像是其实例的数据类型的类型匹配。这允许 Mirror 支持所有类型的 ADT。
  • 和或乘积没有不同的表示类型(即在 Shapeless 的 Scala 2 版本中没有 HListCoproduct 类型)。相反,数据类型的子类型集合由一个普通(可能带有参数)的元组类型表示。Scala 3 的元编程设施可用于按原样处理这些元组类型,并且可以在它们之上构建更高级别的库。
  • 对于乘积类型和和类型,MirroredElemTypes 的元素按照定义顺序排列(即 Tree 中的 Branch[T]MirroredElemTypes 中位于 Leaf[T] 之前,因为在源文件中 BranchLeaf 之前定义)。这意味着 Mirror.Sum 在这一点上不同于 Shapeless 在 Scala 2 中对 ADT 的泛型表示,其中构造函数按名称按字母顺序排列。
  • 方法 ordinalfromProduct 根据 MirroredMonoType 定义,MirroredMonoType 是通过对 MirroredType 的类型参数使用通配符而获得的类型 *

使用 Mirror 实现 derived

如前所见,类型类 TC[_]derived 方法的签名和实现是任意的,但我们希望它通常采用以下形式

import scala.deriving.Mirror

inline def derived[T](using Mirror.Of[T]): TC[T] = ...

也就是说,derived 方法采用类型 Mirror 的(某种子类型)上下文参数,该参数定义派生类型 T 的形状,并根据该形状计算类型类实现。这是具有 derives 子句的 ADT 提供者必须了解的有关类型类实例派生的全部内容。

请注意,derived 方法可能间接具有上下文 Mirror 参数(例如,通过具有一个上下文参数,该参数又具有一个上下文 Mirror 参数,或者根本没有(例如,它们可能使用一些完全不同的用户提供的机制,例如使用 Scala 3 宏或运行时反射)。我们希望基于(直接或间接)Mirror 的实现是最常见的,这也是本文的重点。

类型类作者很可能使用更高级别的派生或泛型编程库来实现 derived 方法。下面提供了一个示例,说明如何仅使用上面描述的低级工具和 Scala 3 的一般元编程特性来实现 derived 方法。预计类型类作者通常不会以这种方式实现 derived 方法,但是,此演练可以作为我们希望典型的类型类作者将使用的更高级别派生库的作者的指南(有关此类库的完整示例,请参阅 Shapeless 3)。

如何使用低级机制编写类型类 derived 方法

我们将在此示例中用于实现类型类 derived 方法的低级技术利用了 Scala 3 中的三个新的类型级构造:内联方法、内联匹配以及通过 summonInlinesummonFrom 进行的隐式搜索。给定 Eq 类型类的此定义,

trait Eq[T]:
  def eqv(x: T, y: T): Boolean

我们需要在 Eq 的伴生对象上实现一个方法 Eq.derived,该方法为 Eq[T] 生成一个给定的实例,前提是给定一个 Mirror[T]。以下是一个可能的实现,

import scala.deriving.Mirror

inline def derived[T](using m: Mirror.Of[T]): Eq[T] =
  lazy val elemInstances = summonInstances[T, m.MirroredElemTypes] // (1)
  inline m match                                                   // (2)
    case s: Mirror.SumOf[T]     => eqSum(s, elemInstances)
    case p: Mirror.ProductOf[T] => eqProduct(p, elemInstances)

请注意,derived 被定义为 inline def。这意味着该方法将在所有调用站点内联(例如,编译器生成的实例定义在具有 deriving Eq 子句的 ADT 的伴生对象中)。

如果过度使用,复杂代码的内联可能会很昂贵(意味着编译时间更慢),因此我们应该小心限制对同一类型的 derived 的调用次数。例如,在为和类型计算实例时,可能需要递归调用 derived 来为其每个子案例计算实例。该子案例反过来可能是一个乘积类型,它声明了一个引用回父和类型的字段。要计算此字段的实例,我们不应递归调用 derived,而应从上下文中调用。通常,找到的给定实例将是最初调用 derived 的根给定实例。

derived (1) 的主体首先实现要为其派生实例的所有子类型的 Eq 实例。这可能是和类型的全部分支或乘积类型的全部字段。summonInstances 的实现是 inline,并使用 Scala 3 的 summonInline 构造将实例收集为 List

inline def summonInstances[T, Elems <: Tuple]: List[Eq[?]] =
  inline erasedValue[Elems] match
    case _: (elem *: elems) => deriveOrSummon[T, elem] :: summonInstances[T, elems]
    case _: EmptyTuple => Nil

inline def deriveOrSummon[T, Elem]: Eq[Elem] =
  inline erasedValue[Elem] match
    case _: T => deriveRec[T, Elem]
    case _    => summonInline[Eq[Elem]]

inline def deriveRec[T, Elem]: Eq[Elem] =
  inline erasedValue[T] match
    case _: Elem => error("infinite recursive derivation")
    case _       => Eq.derived[Elem](using summonInline[Mirror.Of[Elem]]) // recursive derivation

有了子实例,derived 方法使用 inline match 分派到可以为和或乘积构造实例的方法 (2)。请注意,因为 derivedinline,所以匹配将在编译时解析,并且只有匹配案例的右侧将内联到生成的代码中,类型会根据匹配结果进行优化。

在和案例 eqSum 中,我们使用 eqv 参数的运行时 ordinal 值首先检查两个值是否属于 ADT 的相同子类型 (3),然后,如果是,则使用辅助方法 check (4) 根据适当的 ADT 子类型的 Eq 实例进一步测试相等性。

import scala.deriving.Mirror

def eqSum[T](s: Mirror.SumOf[T], elems: => List[Eq[?]]): Eq[T] =
  new Eq[T]:
    def eqv(x: T, y: T): Boolean =
      val ordx = s.ordinal(x)                            // (3)
      (s.ordinal(y) == ordx) && check(x, y, elems(ordx)) // (4)

在乘积案例 eqProduct 中,我们根据数据类型的字段的 Eq 实例,测试 eqv 参数的运行时值是否相等,作为乘积 (5),

import scala.deriving.Mirror

def eqProduct[T](p: Mirror.ProductOf[T], elems: => List[Eq[?]]): Eq[T] =
  new Eq[T]:
    def eqv(x: T, y: T): Boolean =
      iterable(x).lazyZip(iterable(y)).lazyZip(elems).forall(check)

eqSumeqProduct 都有一个按名称命名的参数 elems,因为传递的参数是对惰性 elemInstances 值的引用。

将所有这些放在一起,我们有以下完整的实现,

import scala.collection.AbstractIterable
import scala.compiletime.{erasedValue, error, summonInline}
import scala.deriving.*

inline def summonInstances[T, Elems <: Tuple]: List[Eq[?]] =
  inline erasedValue[Elems] match
    case _: (elem *: elems) => deriveOrSummon[T, elem] :: summonInstances[T, elems]
    case _: EmptyTuple => Nil

inline def deriveOrSummon[T, Elem]: Eq[Elem] =
  inline erasedValue[Elem] match
    case _: T => deriveRec[T, Elem]
    case _    => summonInline[Eq[Elem]]

inline def deriveRec[T, Elem]: Eq[Elem] =
  inline erasedValue[T] match
    case _: Elem => error("infinite recursive derivation")
    case _       => Eq.derived[Elem](using summonInline[Mirror.Of[Elem]]) // recursive derivation

trait Eq[T]:
  def eqv(x: T, y: T): Boolean

object Eq:
  given Eq[Int] with
    def eqv(x: Int, y: Int) = x == y

  def check(x: Any, y: Any, elem: Eq[?]): Boolean =
    elem.asInstanceOf[Eq[Any]].eqv(x, y)

  def iterable[T](p: T): Iterable[Any] = new AbstractIterable[Any]:
    def iterator: Iterator[Any] = p.asInstanceOf[Product].productIterator

  def eqSum[T](s: Mirror.SumOf[T], elems: => List[Eq[?]]): Eq[T] =
    new Eq[T]:
      def eqv(x: T, y: T): Boolean =
        val ordx = s.ordinal(x)
        (s.ordinal(y) == ordx) && check(x, y, elems(ordx))

  def eqProduct[T](p: Mirror.ProductOf[T], elems: => List[Eq[?]]): Eq[T] =
    new Eq[T]:
      def eqv(x: T, y: T): Boolean =
        iterable(x).lazyZip(iterable(y)).lazyZip(elems).forall(check)

  inline def derived[T](using m: Mirror.Of[T]): Eq[T] =
    lazy val elemInstances = summonInstances[T, m.MirroredElemTypes]
    inline m match
      case s: Mirror.SumOf[T]     => eqSum(s, elemInstances)
      case p: Mirror.ProductOf[T] => eqProduct(p, elemInstances)
end Eq

我们可以针对一个简单的 ADT 进行测试,如下所示,

enum Lst[+T] derives Eq:
  case Cns(t: T, ts: Lst[T])
  case Nl

extension [T](t: T) def ::(ts: Lst[T]): Lst[T] = Lst.Cns(t, ts)

@main def test(): Unit =
  import Lst.*
  val eqoi = summon[Eq[Lst[Int]]]
  assert(eqoi.eqv(23 :: 47 :: Nl, 23 :: 47 :: Nl))
  assert(!eqoi.eqv(23 :: Nl, 7 :: Nl))
  assert(!eqoi.eqv(23 :: Nl, Nl))

在这种情况下,经过一些修改后,为 Lst 导出的 Eq 实例的内联展开所生成的代码如下所示,

given derived$Eq[T](using eqT: Eq[T]): Eq[Lst[T]] =
  eqSum(summon[Mirror.Of[Lst[T]]], {/* cached lazily */
    List(
      eqProduct(summon[Mirror.Of[Cns[T]]], {/* cached lazily */
        List(summon[Eq[T]], summon[Eq[Lst[T]]])
      }),
      eqProduct(summon[Mirror.Of[Nl.type]], {/* cached lazily */
        Nil
      })
    )
  })

elemInstances 上的 lazy 修饰符对于防止递归类型(如 Lst)的导出实例中的无限递归是必需的。

可以采用其他方法来定义 derived 方法。例如,更激进地内联使用 Scala 3 宏的变体,虽然对于类型类作者来说比上面的示例更难写,但可以生成像 Eq 这样的类型类的代码,这些代码消除了所有抽象工件(例如,上面子实例的 Lists),并生成与程序员可能手动编写的代码无法区分的代码。作为第三个示例,使用 Shapeless 等更高级别的库,类型类作者可以将等效的 derived 方法定义为,

given eqSum[A](using inst: => K0.CoproductInstances[Eq, A]): Eq[A] with
  def eqv(x: A, y: A): Boolean = inst.fold2(x, y)(false)(
    [t] => (eqt: Eq[t], t0: t, t1: t) => eqt.eqv(t0, t1)
  )

given eqProduct[A](using inst: => K0.ProductInstances[Eq, A]): Eq[A] with
  def eqv(x: A, y: A): Boolean = inst.foldLeft2(x, y)(true: Boolean)(
    [t] => (acc: Boolean, eqt: Eq[t], t0: t, t1: t) =>
      Complete(!eqt.eqv(t0, t1))(false)(true)
  )

inline def derived[A](using gen: K0.Generic[A]): Eq[A] =
  gen.derive(eqProduct, eqSum)

此处描述的框架支持这三种方法,而无需强制使用任何一种方法。

有关如何使用宏编写类型类 derived 方法的简要讨论,请阅读 如何使用宏编写类型类 derived 方法 了解更多信息。

语法

Template          ::=  InheritClauses [TemplateBody]
EnumDef           ::=  id ClassConstr InheritClauses EnumBody
InheritClauses    ::=  [‘extends’ ConstrApps] [‘derives’ QualId {‘,’ QualId}]
ConstrApps        ::=  ConstrApp {‘with’ ConstrApp}
                    |  ConstrApp {‘,’ ConstrApp}

注意:为了对齐 extends 子句和 derives 子句,Scala 3 还允许使用逗号分隔多个扩展类型。因此,以下内容现在是合法的

class A extends B, C { ... }

它等效于旧形式

class A extends B with C { ... }

讨论

此类型类导出框架故意非常小且底层。编译器生成的 Mirror 实例中基本上有两部分基础设施,

  • 对镜像类型的属性进行编码的类型成员。
  • 用于以泛型方式处理镜像类型项的最小值级机制。

Mirror 基础设施可以看作是用例类的现有 Product 基础设施的扩展:通常,Mirror 类型将由 ADT 伴随对象实现,因此类型成员和 ordinalfromProduct 方法将是该对象中的成员。做出此设计决策的主要动机,以及通过类型而不是项对属性进行编码的决策是保持该特性的字节码和运行时占用空间足够小,从而可以无条件地提供 Mirror 实例。

虽然 Mirrors 通过类型成员精确地对属性进行编码,但值级 ordinalfromProduct 有点类型弱(因为它们是根据 MirroredMonoType 定义的),就像 Product 的成员一样。这意味着泛型类型类的代码必须确保类型探索和值选择同步进行,并且它必须在某些地方使用强制转换来断言此一致性。如果泛型类型类编写正确,这些强制转换永远不会失败。

然而,如上所述,编译器提供的机制故意非常底层,并且预计更高级别的类型类派生和泛型编程库将基于此和 Scala 3 的其他元编程工具来向类型类作者和普通用户隐藏这些底层细节。Shapeless 和 Magnolia 风格的类型类派生是可能的(Shapeless 3 的原型,它结合了 Shapeless 2 和 Magnolia 的方面,已与该语言特性一起开发),就像 Scala 3 的新引用/拼接宏和内联工具支持的更激进的内联风格一样。