Skip to content

Commit e419209

Browse files
committed
Add documentation on derivation with macros
1 parent 53b5ddf commit e419209

File tree

6 files changed

+238
-4
lines changed

6 files changed

+238
-4
lines changed
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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+
```

docs/docs/reference/contextual/derivation.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,10 @@ inline def derived[A](given gen: K0.Generic[A]): Eq[A] = gen.derive(eqSum, eqPro
347347

348348
The framework described here enables all three of these approaches without mandating any of them.
349349

350+
For a brief discussion on how to use macros to write a type class `derived`
351+
method please read more at [How to write a type class `derived` method using
352+
macros](./derivation-macro.md).
353+
350354
### Deriving instances elsewhere
351355

352356
Sometimes one would like to derive a type class instance for an ADT after the ADT is defined, without being able to

docs/docs/reference/metaprogramming/macros.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ sum
569569
### Find implicits within a macro
570570

571571
Similarly to the `summonFrom` construct, it is possible to make implicit search available
572-
in a quote context. For this we simply provide `scala.quoted.matching.summonExpr:
572+
in a quote context. For this we simply provide `scala.quoted.matching.summonExpr`:
573573

574574
```scala
575575
inline def setFor[T]: Set[T] = ${ setForExpr[T] }

tests/run-macros/i8007/Macro_1.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import scala.deriving._
22
import scala.quoted._
33
import scala.quoted.matching._
4-
import scala.compiletime.{erasedValue, summonFrom, constValue}
54

65
object Macro1 {
76
case class Person(name: String, age: Int)

tests/run-macros/i8007/Macro_2.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import scala.deriving._
22
import scala.quoted._
33
import scala.quoted.matching._
4-
import scala.compiletime.{erasedValue, summonFrom, constValue}
54

65
object Macro2 {
76

tests/run-macros/i8007/Macro_3.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import scala.deriving._
22
import scala.quoted._
33
import scala.quoted.matching._
4-
import scala.compiletime.{erasedValue, summonFrom, constValue}
54

65
object Macro3 {
76

0 commit comments

Comments
 (0)