如何使用巨集撰寫型別類別 `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