在 GitHub 上編輯此頁面

如何使用巨集撰寫型別類別 `derived` 方法

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