Edit this page on GitHub

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 method 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 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 a method Eq.derived on the companion object of Eq that produces a quoted instance for Eq[T]. Here is a possible signature,

given derived[T: Type](using Quotes): Expr[Eq[T]]

and for comparison reasons we give the same signature we had with inline:

inline given derived[T](using Mirror.Of[T]): Eq[T] = ???

Note, that since a type is used in a subsequent stage it will need to be lifted to a Type by using the corresponding context bound. Also, note that we can summon the quoted Mirror inside the body of the derived thus we can omit it from the signature. The body of the derived method is shown below:

given derived[T: Type](using Quotes): Expr[Eq[T]] =
  import quotes.reflect.*

  val ev: Expr[Mirror.Of[T]] = Expr.summon[Mirror.Of[T]].get

  ev match
    case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} =>
      val elemInstances = summonAll[elementTypes]
      def eqProductBody(x: Expr[Product], y: Expr[Product])(using Quotes): Expr[Boolean] = {
        elemInstances.zipWithIndex.foldLeft(Expr(true)) {
          case (acc, ('{ $elem: Eq[t] }, index)) =>
            val indexExpr = Expr(index)
            val e1 = '{ $x.productElement($indexExpr).asInstanceOf[t] }
            val e2 = '{ $y.productElement($indexExpr).asInstanceOf[t] }
            '{ $acc && $elem.eqv($e1, $e2) }
         }
      }
      '{ eqProduct((x: T, y: T) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) }

  // case for Mirror.ProductOf[T]
  // ...

Note, that in the inline case we can merely write summonAll[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. Since the path inside the type argument is not stable this cannot be used:

'{
  summonAll[$m.MirroredElemTypes]
}

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 summonAll as a macro. We assume that given instances for our primitive types exist.

def summonAll[T: Type](using Quotes): List[Expr[Eq[_]]] =
   Type.of[T] match
      case '[String *: tpes] => '{ summon[Eq[String]] } :: summonAll[tpes]
      case '[Int *: tpes]    => '{ summon[Eq[Int]] }    :: summonAll[tpes]
      case '[tpe *: tpes]    => derived[tpe] :: summonAll[tpes]
      case '[EmptyTuple]     => Nil

One additional difference with the body of derived here as opposed to the one with inline is that with macros we need to synthesize the body of the code during the macro-expansion time. That is the rationale behind the eqProductBody function. Assuming that we calculate the equality of two Persons defined with a case class that holds a name of type String and an age of type Int, the equality check we want to generate is the following:

true
   && Eq[String].eqv(x.productElement(0),y.productElement(0))
   && Eq[Int].eqv(x.productElement(1), y.productElement(1))

Calling the derived method inside the macro

Following the rules in Macros we create two methods. One that hosts the top-level splice eqv and one that is the implementation. Alternatively and what is shown below is that we can call the eqv method directly. The eqGen can trigger the derivation.

extension [T](inline x: T)
   inline def === (inline y: T)(using eq: Eq[T]): Boolean = eq.eqv(x, y)

inline given eqGen[T]: Eq[T] = ${ Eq.derived[T] }

Note, that we use inline method syntax and we can compare instance such as Sm(Person("Test", 23)) === Sm(Person("Test", 24)) for e.g., the following two types:

case class Person(name: String, age: Int)

enum Opt[+T]:
   case Sm(t: T)
   case Nn

The full code is shown below:

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 summonAll[T: Type](using Quotes): List[Expr[Eq[_]]] =
      Type.of[T] match
         case '[String *: tpes] => '{ summon[Eq[String]] } :: summonAll[tpes]
         case '[Int *: tpes]    => '{ summon[Eq[Int]] }    :: summonAll[tpes]
         case '[tpe *: tpes]    => derived[tpe] :: summonAll[tpes]
         case '[EmptyTuple]     => Nil

   given derived[T: Type](using q: Quotes): Expr[Eq[T]] =
      import quotes.reflect.*

      val ev: Expr[Mirror.Of[T]] = Expr.summon[Mirror.Of[T]].get

      ev match
         case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} =>
            val elemInstances = summonAll[elementTypes]
            val eqProductBody: (Expr[T], Expr[T]) => Expr[Boolean] = (x, y) =>
               elemInstances.zipWithIndex.foldLeft(Expr(true: Boolean)) {
                  case (acc, (elem, index)) =>
                     val e1 = '{$x.asInstanceOf[Product].productElement(${Expr(index)})}
                     val e2 = '{$y.asInstanceOf[Product].productElement(${Expr(index)})}

                     '{ $acc && $elem.asInstanceOf[Eq[Any]].eqv($e1, $e2) }
               }
            '{ eqProduct((x: T, y: T) => ${eqProductBody('x, 'y)}) }

         case '{ $m: Mirror.SumOf[T] { type MirroredElemTypes = elementTypes }} =>
            val elemInstances = summonAll[elementTypes]
            val eqSumBody: (Expr[T], Expr[T]) => Expr[Boolean] = (x, y) =>
               val ordx = '{ $m.ordinal($x) }
               val ordy = '{ $m.ordinal($y) }

               val elements = Expr.ofList(elemInstances)
               '{ $ordx == $ordy && $elements($ordx).asInstanceOf[Eq[Any]].eqv($x, $y) }

         '{ eqSum((x: T, y: T) => ${eqSumBody('x, 'y)}) }
   end derived
end Eq

object Macro3:
   extension [T](inline x: T)
      inline def === (inline y: T)(using eq: Eq[T]): Boolean = eq.eqv(x, y)

   inline given eqGen[T]: Eq[T] = ${ Eq.derived[T] }