|
| 1 | +--- |
| 2 | +layout: doc-page |
| 3 | +title: How to write a type class `derived` method using macros |
| 4 | +--- |
| 5 | + |
| 6 | +In the main [derivation](./derivation.md) documentation page we explaind the |
| 7 | +details behind `Mirror`s and type class derivation. Here we demonstrate how to |
| 8 | +implement a type class `derived` method using macros only. We follow the same |
| 9 | +example of deriving `Eq` instances and for simplicity we support a `Product` |
| 10 | +type e.g., a case class `Person`. The low-level method we will use to implement |
| 11 | +the `derived` method exploits quotes, splices of both expressions and types and |
| 12 | +the `scala.quoted.matching.summonExpr` method which is the equivalent of |
| 13 | +`summonFrom`. The former is suitable for use in a quote context, used within |
| 14 | +macros. |
| 15 | + |
| 16 | +As in the original code, the type class definition is the same: |
| 17 | + |
| 18 | +```scala |
| 19 | +trait Eq[T] { |
| 20 | + def eqv(x: T, y: T): Boolean |
| 21 | +} |
| 22 | +``` |
| 23 | + |
| 24 | +we need to implement a method `Eq.derived` on the companion object of `Eq` that |
| 25 | +produces an instance for `Eq[T]` given a `Mirror[T]`. Here is a possible |
| 26 | +signature, |
| 27 | + |
| 28 | +```scala |
| 29 | +def derived[T: Type](ev: Expr[Mirror.Of[T]])(given qctx: QuoteContext): Expr[Eq[T]] = ??? |
| 30 | +``` |
| 31 | + |
| 32 | +and for comparison reasons we give the same signature with had with `inline`: |
| 33 | + |
| 34 | +```scala |
| 35 | +inline given derived[T]: (m: Mirror.Of[T]) => Eq[T] = ??? |
| 36 | +``` |
| 37 | + |
| 38 | +Note, that since a type is used in a subsequent stage it will need to be lifted |
| 39 | +to a `Type` by using the corresponding context bound. The body of this method is |
| 40 | +shown below: |
| 41 | + |
| 42 | + |
| 43 | +```scala |
| 44 | +def derived[T: Type](m: Expr[Mirror.Of[T]])(given qctx: QuoteContext): Expr[Eq[T]] = { |
| 45 | + import qctx.tasty.{_, given} |
| 46 | + |
| 47 | + val elementTypes = m match { |
| 48 | + case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = $elem } } => elem |
| 49 | + } |
| 50 | + |
| 51 | + val elemInstances = summonAll(elementTypes) |
| 52 | + |
| 53 | + val eqProductBody: (Expr[T], Expr[T]) => Expr[Boolean] = (x, y) => { |
| 54 | + elemInstances.zipWithIndex.foldLeft(Expr(true: Boolean)) { |
| 55 | + case (acc, (elem, index)) => |
| 56 | + val e1 = '{$x.asInstanceOf[Product].productElement(${Expr(index)})} |
| 57 | + val e2 = '{$y.asInstanceOf[Product].productElement(${Expr(index)})} |
| 58 | + '{ $acc && $elem.asInstanceOf[Eq[Any]].eqv($e1, $e2) } |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + '{ |
| 63 | + eqProduct((x: T, y: T) => ${eqProductBody('x, 'y)}) |
| 64 | + } |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +Note, that in the `inline` case we can merely write |
| 69 | +`summonAll[m.MirroredElemTypes]` inside the inline method but here, since |
| 70 | +`summonExpr` is required if we need to query the context we need to extract the |
| 71 | +element types in a macro fashion. Being inside a macro, our first reaction would |
| 72 | +be to write the code below. Since the path inside the type argument is not |
| 73 | +stable this cannot be used: |
| 74 | + |
| 75 | +```scala |
| 76 | +'{ |
| 77 | + summonAll[$m.MirroredElemTypes] |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +Instead we extract the tuple-type for element types using pattern matching over |
| 82 | +quotes and more specifically of the refined type: |
| 83 | + |
| 84 | +```scala |
| 85 | + case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = $elem } } => elem |
| 86 | +``` |
| 87 | + |
| 88 | +The implementation of `summonAll` as a macro can be show below: |
| 89 | + |
| 90 | +```scala |
| 91 | +def summonAll[T](t: Type[T])(given qctx: QuoteContext): List[Expr[Eq[_]]] = t match { |
| 92 | + case '[$tpe *: $tpes] => summonExpr(given '[Eq[$tpe]]).get :: summonAll(tpes) |
| 93 | + case '[Unit] => Nil |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +Note, that in a realistic implementation the `summonExpr(given '[Eq[$tpe]]).get` |
| 98 | +is going to fail if the necessary given instances for some type are not present. |
| 99 | + |
| 100 | +One additional difference with the body of `derived` here as opposed to the one |
| 101 | +with `inline` is that with macros we need to synthesize the body of the code during the |
| 102 | +macro-expansion time. That is the rationale behind the `eqProductBody` function. |
| 103 | +Assuming that we calculate the equality of two `Person`s defined with a case |
| 104 | +class that holds a name of type `String` and an age of type `Int`, the equality |
| 105 | +check we want to generate is the following: |
| 106 | + |
| 107 | +```scala |
| 108 | +true |
| 109 | + && Eq[String].eqv(x.productElement(0),y.productElement(0)) |
| 110 | + && Eq[Int].eqv(x.productElement(1), y.productElement(1)) |
| 111 | +``` |
| 112 | + |
| 113 | +### Calling the derived method inside the macro |
| 114 | + |
| 115 | +Following the rules in [Macros](../metaprogramming.md) we create two methods. |
| 116 | +One that hosts the top-level splice `eqv` and one that is the implementation. |
| 117 | + |
| 118 | +```scala |
| 119 | +inline def eqv[T](value: =>T, value2: =>T): Boolean = ${ eqvImpl('value, 'value2) } |
| 120 | + |
| 121 | +def eqvImpl[T: Type](value: Expr[T], value2: Expr[T])(given qctx: QuoteContext): Expr[Boolean] = { |
| 122 | + import qctx.tasty.{_, given} |
| 123 | + |
| 124 | + val mirrorTpe = '[Mirror.Of[T]] |
| 125 | + val mirrorExpr = summonExpr(given mirrorTpe).get |
| 126 | + val derivedInstance = Eq.derived(mirrorExpr) |
| 127 | + |
| 128 | + '{ |
| 129 | + $derivedInstance.eqv($value, $value2) |
| 130 | + } |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | +Note, that we need to quote the type we need `Mirror.Of[T]` with the quoted |
| 135 | +syntax for types and then trigger its synthesis with `summonExpr`. `mirrorExpr` |
| 136 | +now holds the refined type for e.g., a `Person`: |
| 137 | + |
| 138 | +```scala |
| 139 | +scala.deriving.Mirror { |
| 140 | + type MirroredType >: Person <: Person |
| 141 | + type MirroredMonoType >: Person <: Person |
| 142 | + type MirroredElemTypes >: scala.Nothing <: scala.Tuple |
| 143 | +} & scala.deriving.Mirror.Product { |
| 144 | + type MirroredMonoType >: Person <: Person |
| 145 | + type MirroredType >: Person <: Person |
| 146 | + type MirroredLabel >: "Person" <: "Person" |
| 147 | +} { |
| 148 | + type MirroredElemTypes >: scala.*:[scala.Predef.String, scala.*:[scala.Int, scala.Unit]] <: scala.*:[scala.Predef.String, scala.*:[scala.Int, scala.Unit]] |
| 149 | + type MirroredElemLabels >: scala.*:["name", scala.*:["age", scala.Unit]] <: scala.*:["name", scala.*:["age", scala.Unit]] |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +The derived instance then is finally generated with: |
| 154 | + |
| 155 | +```scala |
| 156 | + val derivedInstance = Eq.derived(mirrorExpr) |
| 157 | + |
| 158 | + '{ |
| 159 | + $derivedInstance.eqv($value, $value2) |
| 160 | + } |
| 161 | +``` |
| 162 | + |
| 163 | +The full code is shown below: |
| 164 | + |
| 165 | +```scala |
| 166 | +import scala.deriving._ |
| 167 | +import scala.quoted._ |
| 168 | +import scala.quoted.matching._ |
| 169 | + |
| 170 | +object Macro { |
| 171 | + |
| 172 | + trait Eq[T] { |
| 173 | + def eqv(x: T, y: T): Boolean |
| 174 | + } |
| 175 | + |
| 176 | + object Eq { |
| 177 | + given Eq[String] { |
| 178 | + def eqv(x: String, y: String) = x == y |
| 179 | + } |
| 180 | + |
| 181 | + given Eq[Int] { |
| 182 | + def eqv(x: Int, y: Int) = x == y |
| 183 | + } |
| 184 | + |
| 185 | + def eqProduct[T](body: (T, T) => Boolean): Eq[T] = |
| 186 | + new Eq[T] { |
| 187 | + def eqv(x: T, y: T): Boolean = body(x, y) |
| 188 | + } |
| 189 | + |
| 190 | + def summonAll[T](t: Type[T])(given qctx: QuoteContext): List[Expr[Eq[_]]] = t match { |
| 191 | + case '[$tpe *: $tpes] => summonExpr(given '[Eq[$tpe]]).get :: summonAll(tpes) |
| 192 | + case '[Unit] => Nil |
| 193 | + } |
| 194 | + |
| 195 | + def derived[T: Type](ev: Expr[Mirror.Of[T]])(given qctx: QuoteContext): Expr[Eq[T]] = { |
| 196 | + import qctx.tasty.{_, given} |
| 197 | + |
| 198 | + val elementTypes = ev match { |
| 199 | + case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = $elem } } => elem |
| 200 | + } |
| 201 | + |
| 202 | + val elemInstances = summonAll(elementTypes) |
| 203 | + |
| 204 | + val eqProductBody: (Expr[T], Expr[T]) => Expr[Boolean] = (x, y) => { |
| 205 | + elemInstances.zipWithIndex.foldLeft(Expr(true: Boolean)) { |
| 206 | + case (acc, (elem, index)) => |
| 207 | + val e1 = '{$x.asInstanceOf[Product].productElement(${Expr(index)})} |
| 208 | + val e2 = '{$y.asInstanceOf[Product].productElement(${Expr(index)})} |
| 209 | + '{ $acc && $elem.asInstanceOf[Eq[Any]].eqv($e1, $e2) } |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + '{ |
| 214 | + eqProduct((x: T, y: T) => ${eqProductBody('x, 'y)}) |
| 215 | + } |
| 216 | + } |
| 217 | + } |
| 218 | + |
| 219 | + inline def eqv[T](value: =>T, value2: =>T): Boolean = ${ eqvImpl('value, 'value2) } |
| 220 | + |
| 221 | + def eqvImpl[T: Type](value: Expr[T], value2: Expr[T])(given qctx: QuoteContext): Expr[Boolean] = { |
| 222 | + import qctx.tasty.{_, given} |
| 223 | + |
| 224 | + val mirrorTpe = '[Mirror.Of[T]] |
| 225 | + val mirrorExpr = summonExpr(given mirrorTpe).get |
| 226 | + val derivedInstance = Eq.derived(mirrorExpr) |
| 227 | + |
| 228 | + '{ |
| 229 | + $derivedInstance.eqv($value, $value2) |
| 230 | + } |
| 231 | + } |
| 232 | +} |
| 233 | +``` |
0 commit comments