How to write a type class `derived` method using macros
In the main derivation documentation page, we explained the details behind Mirrors and type class derivation. Here we demonstrate how to implement a type class derived method using macros only. We follow the same example of deriving Eq instances and for simplicity we support a Product type e.g., a case class Person. The low-level technique that we will use to implement the derived method exploits quotes, splices of both expressions and types and the scala.quoted.Expr.summon method which is the equivalent of scala.compiletime.summonFrom. The former is suitable for use in a quote context, used within macros.
As in the original code, the type class definition is the same:
trait Eq[T]:
def eqv(x: T, y: T): Boolean
We need to implement an inline method Eq.derived on the companion object of Eq that calls into a macro to produce a quoted instance for Eq[T]. Here is a possible signature:
inline def derived[T]: Eq[T] = ${ derivedMacro[T] }
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] = ???
Note, that since a type is used in a subsequent macro compilation stage it will need to be lifted to a quoted.Type by using the corresponding context bound (seen in derivedMacro).
For comparison, here is the signature of the inline derived method from the main derivation page:
inline def derived[T](using m: Mirror.Of[T]): Eq[T] = ???
Note that the macro-based derived signature does not have a Mirror parameter. This is because we can summon the Mirror inside the body of derivedMacro thus we can omit it from the signature.
One additional possibility with the body of derivedMacro here as opposed to the one with inline is that with macros it is simpler to create a fully optimised method body for eqv.
Let's say we wanted to derive an Eq instance for the following case class Person,
case class Person(name: String, age: Int) derives Eq
the equality check we are going to generate is the following:
(x: Person, y: Person) =>
summon[Eq[String]].eqv(x.productElement(0), y.productElement(0))
&& summon[Eq[Int]].eqv(x.productElement(1), y.productElement(1))
Note that it is possible, by using the reflection API, to further optimise and directly reference the fields of
Person, but for clear understanding we will only use quoted expressions.
The code to generates this body can be seen in the eqProductBody method, shown here as part of the definition for the derivedMacro method:
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] ...
Note, that in the version without macros, we can merely write summonInstances[T, m.MirroredElemTypes] inside the inline method but here, since Expr.summon is required, we can extract the element types in a macro fashion. Being inside a macro, our first reaction would be to write the code below:
'{
summonInstances[T, $m.MirroredElemTypes]
}
However, since the path inside the type argument is not stable this cannot be used. Instead we extract the tuple-type for element types using pattern matching over quotes and more specifically of the refined type:
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} => ...
Shown below is the implementation of summonInstances as a macro, which for each type elem in the tuple type, calls deriveOrSummon[T, elem].
To understand deriveOrSummon, consider that if elem derives from the parent T type, then it is a recursive derivation. Recursive derivation usually happens for types such as scala.collection.immutable.::. If elem does not derive from T, then there must exist a contextual Eq[elem] instance.
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
The full code is shown below:
import compiletime.*
import scala.deriving.*
import scala.quoted.*
trait Eq[T]:
def eqv(x: T, y: T): Boolean
object Eq:
given Eq[String]:
def eqv(x: String, y: String) = x == y
given Eq[Int]:
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