如何使用宏编写类型类“派生”方法
在 derivation 主文档页面中,我们解释了 Mirror
和类型类派生的详细信息。在此,我们演示如何仅使用宏实现类型类 derived
方法。我们遵循派生 Eq
实例的相同示例,为了简单起见,我们支持 Product
类型,例如,案例类 Person
。我们将用来实现 derived
方法的底层技术利用了引用、表达式和类型的拼接以及 scala.quoted.Expr.summon
方法,该方法等效于 scala.compiletime.summonFrom
。前者适用于在宏内使用的引用上下文中。
与原始代码一样,类型类定义相同
trait Eq[T]:
def eqv(x: T, y: T): Boolean
我们需要在 Eq
的伴生对象上实现一个内联方法 Eq.derived
,它调用宏来生成 Eq[T]
的引用实例。以下是可能的签名
inline def derived[T]: Eq[T] = ${ derivedMacro[T] }
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] = ???
注意,由于类型用于后续的宏编译阶段,因此需要使用相应的上下文绑定(在 derivedMacro
中看到)将其提升到 quoted.Type
。
为了进行比较,以下是来自 主推导页 的内联 derived
方法的签名
inline def derived[T](using m: Mirror.Of[T]): Eq[T] = ???
请注意,基于宏的 derived
签名没有 Mirror
参数。这是因为我们可以在 derivedMacro
的主体中调用 Mirror
,因此我们可以从签名中省略它。
与 inline
相比,这里 derivedMacro
主体的另一个可能性是,使用宏可以更简单地为 eqv
创建一个完全优化的函数体。
假设我们想为以下案例类 Person
推导出一个 Eq
实例,
case class Person(name: String, age: Int) derives Eq
我们将生成的相等性检查如下
(x: Person, y: Person) =>
summon[Eq[String]].eqv(x.productElement(0), y.productElement(0))
&& summon[Eq[Int]].eqv(x.productElement(1), y.productElement(1))
请注意,可以使用 反射 API 进一步优化并直接引用
Person
的字段,但为了清楚理解,我们只使用引用表达式。
生成此函数体的代码可以在 eqProductBody
方法中看到,这里显示为 derivedMacro
方法定义的一部分
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] =
val ev: Expr[Mirror.Of[T]] = Expr.summon[Mirror.Of[T]].get
ev match
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} =>
val elemInstances = summonInstances[T, elementTypes]
def eqProductBody(x: Expr[Product], y: Expr[Product])(using Quotes): Expr[Boolean] = {
if elemInstances.isEmpty then
Expr(true)
else
elemInstances.zipWithIndex.map {
case ('{ $elem: Eq[t] }, index) =>
val indexExpr = Expr(index)
val e1 = '{ $x.productElement($indexExpr).asInstanceOf[t] }
val e2 = '{ $y.productElement($indexExpr).asInstanceOf[t] }
'{ $elem.eqv($e1, $e2) }
}.reduce((acc, elem) => '{ $acc && $elem })
end if
}
'{ eqProduct((x: T, y: T) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) }
// case for Mirror.SumOf[T] ...
请注意,在没有宏的版本中,我们只能在内联方法中编写 summonInstances[T, m.MirroredElemTypes]
,但在这里,由于需要 Expr.summon
,我们可以以宏方式提取元素类型。在宏内部,我们的第一个反应是编写以下代码
'{
summonInstances[T, $m.MirroredElemTypes]
}
但是,由于类型参数中的路径不稳定,因此无法使用它。相反,我们使用模式匹配对引用(更具体地说是精炼类型)提取元素类型的元组类型
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} => ...
下面显示了 summonInstances
作为宏的实现,它为元组类型中的每个类型 elem
调用 deriveOrSummon[T, elem]
。
要理解 deriveOrSummon
,请考虑如果 elem
衍生自父类型 T
,则它是一个递归推导。递归推导通常发生在诸如 scala.collection.immutable.::
之类的类型上。如果 elem
不派生自 T
,则必须存在一个上下文 Eq[elem]
实例。
def summonInstances[T: Type, Elems: Type](using Quotes): List[Expr[Eq[?]]] =
Type.of[Elems] match
case '[elem *: elems] => deriveOrSummon[T, elem] :: summonInstances[T, elems]
case '[EmptyTuple] => Nil
def deriveOrSummon[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
Type.of[Elem] match
case '[T] => deriveRec[T, Elem]
case _ => '{ summonInline[Eq[Elem]] }
def deriveRec[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
Type.of[T] match
case '[Elem] => '{ error("infinite recursive derivation") }
case _ => derivedMacro[Elem] // recursive derivation
完整代码如下所示
import compiletime.*
import scala.deriving.*
import scala.quoted.*
trait Eq[T]:
def eqv(x: T, y: T): Boolean
object Eq:
given Eq[String] with
def eqv(x: String, y: String) = x == y
given Eq[Int] with
def eqv(x: Int, y: Int) = x == y
def eqProduct[T](body: (T, T) => Boolean): Eq[T] =
new Eq[T]:
def eqv(x: T, y: T): Boolean = body(x, y)
def eqSum[T](body: (T, T) => Boolean): Eq[T] =
new Eq[T]:
def eqv(x: T, y: T): Boolean = body(x, y)
def summonInstances[T: Type, Elems: Type](using Quotes): List[Expr[Eq[?]]] =
Type.of[Elems] match
case '[elem *: elems] => deriveOrSummon[T, elem] :: summonInstances[T, elems]
case '[EmptyTuple] => Nil
def deriveOrSummon[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
Type.of[Elem] match
case '[T] => deriveRec[T, Elem]
case _ => '{ summonInline[Eq[Elem]] }
def deriveRec[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
Type.of[T] match
case '[Elem] => '{ error("infinite recursive derivation") }
case _ => derivedMacro[Elem] // recursive derivation
inline def derived[T]: Eq[T] = ${ derivedMacro[T] }
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] =
val ev: Expr[Mirror.Of[T]] = Expr.summon[Mirror.Of[T]].get
ev match
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} =>
val elemInstances = summonInstances[T, elementTypes]
def eqProductBody(x: Expr[Product], y: Expr[Product])(using Quotes): Expr[Boolean] = {
if elemInstances.isEmpty then
Expr(true)
else
elemInstances.zipWithIndex.map {
case ('{ $elem: Eq[t] }, index) =>
val indexExpr = Expr(index)
val e1 = '{ $x.productElement($indexExpr).asInstanceOf[t] }
val e2 = '{ $y.productElement($indexExpr).asInstanceOf[t] }
'{ $elem.eqv($e1, $e2) }
}.reduce((acc, elem) => '{ $acc && $elem })
end if
}
'{ eqProduct((x: T, y: T) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) }
case '{ $m: Mirror.SumOf[T] { type MirroredElemTypes = elementTypes }} =>
val elemInstances = summonInstances[T, elementTypes]
val elements = Expr.ofList(elemInstances)
def eqSumBody(x: Expr[T], y: Expr[T])(using Quotes): Expr[Boolean] =
val ordx = '{ $m.ordinal($x) }
val ordy = '{ $m.ordinal($y) }
'{ $ordx == $ordy && $elements($ordx).asInstanceOf[Eq[Any]].eqv($x, $y) }
'{ eqSum((x: T, y: T) => ${eqSumBody('x, 'y)}) }
end derivedMacro
end Eq