在 GitHub 上编辑此页面

如何使用宏编写类型类“派生”方法

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