类型类派生
类型类派生是一种为满足某些简单条件的类型类自动生成给定实例的方法。此意义上的类型类是具有单个类型参数(用于确定正在操作的类型)的任何特征或类,以及特殊情况CanEqual
。常见的示例是Eq
、Ordering
或Show
。例如,给定以下Tree
代数数据类型 (ADT)
enum Tree[T] derives Eq, Ordering, Show:
case Branch(left: Tree[T], right: Tree[T])
case Leaf(elem: T)
derives
子句在Tree
的伴生对象中为Eq
、Ordering
和Show
类型类生成以下给定实例
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
是派生类型,而Eq
、Ordering
和Show
实例是派生实例。
注意:可以手动使用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
定义,则应用重载解析。
派生实例的具体外观取决于 DerivingType
和 TC
的具体情况,我们首先检查 TC
TC
接受 1 个参数 F
因此,TC
被定义为 TC[F[A_1, ..., A_K]]
(如果 K == 0
,则为 TC[F]
),其中 F
是某些内容。根据参数的种类,还有两种进一步的情况
F
和 DerivingType
的所有参数的种类为 *
注意: 在这种情况下,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
F
和 DerivingType
在右侧具有匹配种类的参数
本节涵盖了您可以从右侧开始配对 F
和 DerivingType
的参数的情况,使得它们成对具有相同的种类,并且 F
或 DerivingType
(或两者)的所有参数都用完了。F
还必须至少有一个参数。
然后,一般形状将是
given [...]: TC[ [...] =>> DerivingType[...] ] = TC.derived
当然,TC
和 DerivingType
应用于正确类型的类型。
为了实现此功能,我们将它分成 3 个案例
如果 F
和 DerivingType
采用相同数量的参数(N == K
)
given TC[DerivingType] = TC.derived
// simplified form of:
given TC[ [A_1, ..., A_K] =>> DerivingType[A_1, ..., A_K] ] = TC.derived
如果 DerivingType
采用的参数少于 F
(N < 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
采用的参数少于 DerivingType
(K < N
),我们将剩余的最左侧插槽填入给定类型的类型参数
given [T_1, ... T_(N-K)]: TC[[A_1, ..., A_K] =>> DerivingType[T_1, ... T_(N-K), A_1, ..., A_K]] = TC.derived
TC
是 CanEqual
类型类
因此,我们有:DerivingType[T_1, ..., T_N] 导出 CanEqual
。
令 U_1
, ..., U_M
为 DerivingType
的类型为 *
的参数。(这些是 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 元编程设施的编译时功能。
- 镜像类型是局部类或内部类没有限制。
MirroredType
和MirroredElemTypes
的类型与镜像是其实例的数据类型的类型匹配。这允许Mirror
支持所有类型的 ADT。- 和或乘积没有不同的表示类型(即在 Shapeless 的 Scala 2 版本中没有
HList
或Coproduct
类型)。相反,数据类型的子类型集合由一个普通(可能带有参数)的元组类型表示。Scala 3 的元编程设施可用于按原样处理这些元组类型,并且可以在它们之上构建更高级别的库。 - 对于乘积类型和和类型,
MirroredElemTypes
的元素按照定义顺序排列(即Tree
中的Branch[T]
在MirroredElemTypes
中位于Leaf[T]
之前,因为在源文件中Branch
在Leaf
之前定义)。这意味着Mirror.Sum
在这一点上不同于 Shapeless 在 Scala 2 中对 ADT 的泛型表示,其中构造函数按名称按字母顺序排列。 - 方法
ordinal
和fromProduct
根据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 中的三个新的类型级构造:内联方法、内联匹配以及通过 summonInline
或 summonFrom
进行的隐式搜索。给定 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)。请注意,因为 derived
是 inline
,所以匹配将在编译时解析,并且只有匹配案例的右侧将内联到生成的代码中,类型会根据匹配结果进行优化。
在和案例 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)
eqSum
和 eqProduct
都有一个按名称命名的参数 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 伴随对象实现,因此类型成员和 ordinal
或 fromProduct
方法将是该对象中的成员。做出此设计决策的主要动机,以及通过类型而不是项对属性进行编码的决策是保持该特性的字节码和运行时占用空间足够小,从而可以无条件地提供 Mirror
实例。
虽然 Mirrors
通过类型成员精确地对属性进行编码,但值级 ordinal
和 fromProduct
有点类型弱(因为它们是根据 MirroredMonoType
定义的),就像 Product
的成员一样。这意味着泛型类型类的代码必须确保类型探索和值选择同步进行,并且它必须在某些地方使用强制转换来断言此一致性。如果泛型类型类编写正确,这些强制转换永远不会失败。
然而,如上所述,编译器提供的机制故意非常底层,并且预计更高级别的类型类派生和泛型编程库将基于此和 Scala 3 的其他元编程工具来向类型类作者和普通用户隐藏这些底层细节。Shapeless 和 Magnolia 风格的类型类派生是可能的(Shapeless 3 的原型,它结合了 Shapeless 2 和 Magnolia 的方面,已与该语言特性一起开发),就像 Scala 3 的新引用/拼接宏和内联工具支持的更激进的内联风格一样。