Skip to content

Commit bdd1ade

Browse files
committed
remove 'given derived' from macro typeclass derivation
1 parent c0941e7 commit bdd1ade

File tree

1 file changed

+126
-133
lines changed

1 file changed

+126
-133
lines changed

docs/_docs/reference/contextual/derivation-macro.md

Lines changed: 126 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,11 @@ title: "How to write a type class `derived` method using macros"
44
nightlyOf: https://docs.scala-lang.org/scala3/reference/contextual/derivation-macro.html
55
---
66

7-
In the main [derivation](./derivation.md) documentation page, we explained the
8-
details behind `Mirror`s and type class derivation. Here we demonstrate how to
9-
implement a type class `derived` method using macros only. We follow the same
10-
example of deriving `Eq` instances and for simplicity we support a `Product`
11-
type e.g., a case class `Person`. The low-level method we will use to implement
12-
the `derived` method exploits quotes, splices of both expressions and types and
13-
the `scala.quoted.Expr.summon` method which is the equivalent of
14-
`summonFrom`. The former is suitable for use in a quote context, used within
15-
macros.
7+
In the main [derivation](./derivation.md) documentation page, we explained the details behind `Mirror`s and type class derivation.
8+
Here we demonstrate how to implement a type class `derived` method using macros only.
9+
We follow the same example of deriving `Eq` instances and for simplicity we support a `Product` type e.g., a case class `Person`.
10+
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`.
11+
The former is suitable for use in a quote context, used within macros.
1612

1713
As in the original code, the type class definition is the same:
1814

@@ -21,185 +17,182 @@ trait Eq[T]:
2117
def eqv(x: T, y: T): Boolean
2218
```
2319

24-
we need to implement a method `Eq.derived` on the companion object of `Eq` that
25-
produces a quoted instance for `Eq[T]`. Here is a possible signature,
20+
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]`.
21+
Here is a possible signature:
2622

23+
24+
```scala
25+
inline def derived[T]: Eq[T] = ${ derivedMacro[T] }
26+
27+
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] = ???
28+
```
29+
30+
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`).
31+
32+
33+
For comparison, here is the signature of the inline `derived` method from the [main derivation page](./derivation.md):
34+
```scala
35+
inline def derived[T](using m: Mirror.Of[T]): Eq[T] = ???
36+
```
37+
38+
Note that the macro-based `derived` signature does not have a `Mirror` parameter.
39+
This is because we can summon the `Mirror` inside the body of `derivedMacro` thus we can omit it from the signature.
40+
41+
One additional difference 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`.
42+
43+
Let's say we wanted to derive an `Eq` instance for the following case class `Person`,
2744
```scala
28-
given derived[T: Type](using Quotes): Expr[Eq[T]]
45+
case class Person(name: String, age: Int) derives Eq
2946
```
3047

31-
and for comparison reasons we give the same signature we had with `inline`:
48+
the equality check we want to generate is the following:
3249

3350
```scala
34-
inline given derived[T](using Mirror.Of[T]): Eq[T] = ???
51+
true
52+
&& summon[Eq[String]].eqv(x.productElement(0), y.productElement(0))
53+
&& summon[Eq[Int]].eqv(x.productElement(1), y.productElement(1))
3554
```
3655

37-
Note, that since a type is used in a subsequent stage it will need to be lifted
38-
to a `Type` by using the corresponding context bound. Also, note that we can
39-
summon the quoted `Mirror` inside the body of the `derived` thus we can omit it
40-
from the signature. The body of the `derived` method is shown below:
56+
57+
The code to generates this body can be seen in the `eqProductBody` method, shown here as part of the definition for the `derivedMacro` method:
4158

4259

4360
```scala
44-
given derived[T: Type](using Quotes): Expr[Eq[T]] =
61+
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] =
4562
import quotes.reflect.*
4663

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

4966
ev match
5067
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} =>
51-
val elemInstances = summonAll[elementTypes]
68+
val elemInstances = summonInstances[T, elementTypes]
5269
def eqProductBody(x: Expr[Product], y: Expr[Product])(using Quotes): Expr[Boolean] = {
5370
elemInstances.zipWithIndex.foldLeft(Expr(true)) {
5471
case (acc, ('{ $elem: Eq[t] }, index)) =>
5572
val indexExpr = Expr(index)
5673
val e1 = '{ $x.productElement($indexExpr).asInstanceOf[t] }
5774
val e2 = '{ $y.productElement($indexExpr).asInstanceOf[t] }
5875
'{ $acc && $elem.eqv($e1, $e2) }
59-
}
76+
}
6077
}
61-
'{ eqProduct((x: T, y: T) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) }
78+
'{ eqProduct((x, y) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) }
6279

63-
// case for Mirror.ProductOf[T]
64-
// ...
80+
// case for Mirror.SumOf[T] ...
6581
```
6682

67-
Note, that in the `inline` case we can merely write
68-
`summonAll[m.MirroredElemTypes]` inside the inline method but here, since
69-
`Expr.summon` is required, we can extract the element types in a macro fashion.
70-
Being inside a macro, our first reaction would be to write the code below. Since
71-
the path inside the type argument is not stable this cannot be used:
83+
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.
84+
Being inside a macro, our first reaction would be to write the code below:
7285

7386
```scala
7487
'{
75-
summonAll[$m.MirroredElemTypes]
88+
summonInstances[T, $m.MirroredElemTypes]
7689
}
7790
```
7891

79-
Instead we extract the tuple-type for element types using pattern matching over
80-
quotes and more specifically of the refined type:
92+
However, since the path inside the type argument is not stable this cannot be used.
93+
Instead we extract the tuple-type for element types using pattern matching over quotes and more specifically of the refined type:
8194

8295
```scala
8396
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} => ...
8497
```
8598

86-
Shown below is the implementation of `summonAll` as a macro. We assume that
87-
given instances for our primitive types exist.
99+
Shown below is the implementation of `summonInstances` as a macro, which for each type `elem` in the tuple type, calls
100+
`deriveOrSummon[T, elem]`.
88101

89-
```scala
90-
def summonAll[T: Type](using Quotes): List[Expr[Eq[_]]] =
91-
Type.of[T] match
92-
case '[String *: tpes] => '{ summon[Eq[String]] } :: summonAll[tpes]
93-
case '[Int *: tpes] => '{ summon[Eq[Int]] } :: summonAll[tpes]
94-
case '[tpe *: tpes] => derived[tpe] :: summonAll[tpes]
95-
case '[EmptyTuple] => Nil
96-
```
97-
98-
One additional difference with the body of `derived` here as opposed to the one
99-
with `inline` is that with macros we need to synthesize the body of the code during the
100-
macro-expansion time. That is the rationale behind the `eqProductBody` function.
101-
Assuming that we calculate the equality of two `Person`s defined with a case
102-
class that holds a name of type [`String`](https://scala-lang.org/api/3.x/scala/Predef$.html#String-0)
103-
and an age of type `Int`, the equality check we want to generate is the following:
104-
105-
```scala
106-
true
107-
&& Eq[String].eqv(x.productElement(0),y.productElement(0))
108-
&& Eq[Int].eqv(x.productElement(1), y.productElement(1))
109-
```
110-
111-
## Calling the derived method inside the macro
112-
113-
Following the rules in [Macros](../metaprogramming/metaprogramming.md) we create two methods.
114-
One that hosts the top-level splice `eqv` and one that is the implementation.
115-
Alternatively and what is shown below is that we can call the `eqv` method
116-
directly. The `eqGen` can trigger the derivation.
102+
To understand `deriveOrSummon`, consider that if `elem` derives from the parent `T` type, then it is a recursive derivation.
103+
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.
117104

118105
```scala
119-
extension [T](inline x: T)
120-
inline def === (inline y: T)(using eq: Eq[T]): Boolean = eq.eqv(x, y)
106+
def summonInstances[T: Type, Elems: Type](using Quotes): List[Expr[Eq[?]]] =
107+
Type.of[Elems] match
108+
case '[elem *: elems] => deriveOrSummon[T, elem] :: summonInstances[T, elems]
109+
case '[EmptyTuple] => Nil
121110

122-
inline given eqGen[T]: Eq[T] = ${ Eq.derived[T] }
123-
```
111+
def deriveOrSummon[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
112+
Type.of[Elem] match
113+
case '[T] => deriveRec[T, Elem]
114+
case _ => '{ summonInline[Eq[Elem]] }
124115

125-
Note, that we use inline method syntax and we can compare instance such as
126-
`Sm(Person("Test", 23)) === Sm(Person("Test", 24))` for e.g., the following two
127-
types:
128-
129-
```scala
130-
case class Person(name: String, age: Int)
131-
132-
enum Opt[+T]:
133-
case Sm(t: T)
134-
case Nn
116+
def deriveRec[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
117+
import quotes.reflect.*
118+
Type.of[T] match
119+
case '[Elem] => report.errorAndAbort("infinite recursive derivation")
120+
case _ => derivedMacro[Elem] // recursive derivation
135121
```
136122

137123
The full code is shown below:
138124

139125
```scala
126+
import compiletime.summonInline
140127
import scala.deriving.*
141128
import scala.quoted.*
142129

143130

144131
trait Eq[T]:
145-
def eqv(x: T, y: T): Boolean
132+
def eqv(x: T, y: T): Boolean
146133

147134
object Eq:
148-
given Eq[String] with
149-
def eqv(x: String, y: String) = x == y
150-
151-
given Eq[Int] with
152-
def eqv(x: Int, y: Int) = x == y
153-
154-
def eqProduct[T](body: (T, T) => Boolean): Eq[T] =
155-
new Eq[T]:
156-
def eqv(x: T, y: T): Boolean = body(x, y)
157-
158-
def eqSum[T](body: (T, T) => Boolean): Eq[T] =
159-
new Eq[T]:
160-
def eqv(x: T, y: T): Boolean = body(x, y)
161-
162-
def summonAll[T: Type](using Quotes): List[Expr[Eq[_]]] =
163-
Type.of[T] match
164-
case '[String *: tpes] => '{ summon[Eq[String]] } :: summonAll[tpes]
165-
case '[Int *: tpes] => '{ summon[Eq[Int]] } :: summonAll[tpes]
166-
case '[tpe *: tpes] => derived[tpe] :: summonAll[tpes]
167-
case '[EmptyTuple] => Nil
168-
169-
given derived[T: Type](using q: Quotes): Expr[Eq[T]] =
170-
import quotes.reflect.*
171-
172-
val ev: Expr[Mirror.Of[T]] = Expr.summon[Mirror.Of[T]].get
173-
174-
ev match
175-
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} =>
176-
val elemInstances = summonAll[elementTypes]
177-
val eqProductBody: (Expr[T], Expr[T]) => Expr[Boolean] = (x, y) =>
178-
elemInstances.zipWithIndex.foldLeft(Expr(true: Boolean)) {
179-
case (acc, (elem, index)) =>
180-
val e1 = '{$x.asInstanceOf[Product].productElement(${Expr(index)})}
181-
val e2 = '{$y.asInstanceOf[Product].productElement(${Expr(index)})}
182-
183-
'{ $acc && $elem.asInstanceOf[Eq[Any]].eqv($e1, $e2) }
184-
}
185-
'{ eqProduct((x: T, y: T) => ${eqProductBody('x, 'y)}) }
186-
187-
case '{ $m: Mirror.SumOf[T] { type MirroredElemTypes = elementTypes }} =>
188-
val elemInstances = summonAll[elementTypes]
189-
val eqSumBody: (Expr[T], Expr[T]) => Expr[Boolean] = (x, y) =>
190-
val ordx = '{ $m.ordinal($x) }
191-
val ordy = '{ $m.ordinal($y) }
192-
193-
val elements = Expr.ofList(elemInstances)
194-
'{ $ordx == $ordy && $elements($ordx).asInstanceOf[Eq[Any]].eqv($x, $y) }
195-
196-
'{ eqSum((x: T, y: T) => ${eqSumBody('x, 'y)}) }
197-
end derived
135+
given Eq[String] with
136+
def eqv(x: String, y: String) = x == y
137+
138+
given Eq[Int] with
139+
def eqv(x: Int, y: Int) = x == y
140+
141+
def eqProduct[T](body: (T, T) => Boolean): Eq[T] =
142+
new Eq[T]:
143+
def eqv(x: T, y: T): Boolean = body(x, y)
144+
145+
def eqSum[T](body: (T, T) => Boolean): Eq[T] =
146+
new Eq[T]:
147+
def eqv(x: T, y: T): Boolean = body(x, y)
148+
149+
def summonInstances[T: Type, Elems: Type](using Quotes): List[Expr[Eq[?]]] =
150+
Type.of[Elems] match
151+
case '[elem *: elems] => deriveOrSummon[T, elem] :: summonInstances[T, elems]
152+
case '[EmptyTuple] => Nil
153+
154+
def deriveOrSummon[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
155+
Type.of[Elem] match
156+
case '[T] => deriveRec[T, Elem]
157+
case _ => '{ summonInline[Eq[Elem]] }
158+
159+
def deriveRec[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
160+
import quotes.reflect.*
161+
Type.of[T] match
162+
case '[Elem] => report.errorAndAbort("infinite recursive derivation")
163+
case _ => derivedMacro[Elem] // recursive derivation
164+
165+
inline def derived[T]: Eq[T] = ${ derivedMacro[T] }
166+
167+
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] =
168+
import quotes.reflect.*
169+
170+
val ev: Expr[Mirror.Of[T]] = Expr.summon[Mirror.Of[T]].get
171+
172+
ev match
173+
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} =>
174+
val elemInstances = summonInstances[T, elementTypes]
175+
def eqProductBody(x: Expr[Product], y: Expr[Product])(using Quotes): Expr[Boolean] = {
176+
elemInstances.zipWithIndex.foldLeft(Expr(true)) {
177+
case (acc, ('{ $elem: Eq[t] }, index)) =>
178+
val indexExpr = Expr(index)
179+
val e1 = '{ $x.productElement($indexExpr).asInstanceOf[t] }
180+
val e2 = '{ $y.productElement($indexExpr).asInstanceOf[t] }
181+
'{ $acc && $elem.eqv($e1, $e2) }
182+
}
183+
}
184+
'{ eqProduct((x: T, y: T) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) }
185+
186+
case '{ $m: Mirror.SumOf[T] { type MirroredElemTypes = elementTypes }} =>
187+
val elemInstances = summonInstances[T, elementTypes]
188+
val elements = Expr.ofList(elemInstances)
189+
190+
def eqSumBody(x: Expr[T], y: Expr[T])(using Quotes): Expr[Boolean] =
191+
val ordx = '{ $m.ordinal($x) }
192+
val ordy = '{ $m.ordinal($y) }
193+
'{ $ordx == $ordy && $elements($ordx).asInstanceOf[Eq[Any]].eqv($x, $y) }
194+
195+
'{ eqSum((x: T, y: T) => ${eqSumBody('x, 'y)}) }
196+
end derivedMacro
198197
end Eq
199-
200-
object Macro3:
201-
extension [T](inline x: T)
202-
inline def === (inline y: T)(using eq: Eq[T]): Boolean = eq.eqv(x, y)
203-
204-
inline given eqGen[T]: Eq[T] = ${ Eq.derived[T] }
205198
```

0 commit comments

Comments
 (0)