Skip to content

Commit a85667d

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

File tree

1 file changed

+115
-129
lines changed

1 file changed

+115
-129
lines changed

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

Lines changed: 115 additions & 129 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,175 @@ 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:
22+
2623

2724
```scala
28-
given derived[T: Type](using Quotes): Expr[Eq[T]]
25+
inline def derived[T]: Eq[T] = ${ derivedMacro[T] }
26+
27+
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] = ???
2928
```
3029

31-
and for comparison reasons we give the same signature we had with `inline`:
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+
3232

33+
For comparison, here is the signature of the inline `derived` method from the [main derivation page](./derivation.md):
3334
```scala
34-
inline given derived[T](using Mirror.Of[T]): Eq[T] = ???
35+
inline def derived[T](using m: Mirror.Of[T]): Eq[T] = ???
3536
```
3637

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:
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+
The body of the `derivedMacro` method is shown below:
4141

4242

4343
```scala
44-
given derived[T: Type](using Quotes): Expr[Eq[T]] =
44+
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] =
4545
import quotes.reflect.*
4646

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

4949
ev match
5050
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} =>
51-
val elemInstances = summonAll[elementTypes]
51+
val elemInstances = summonInstances[T, elementTypes]
5252
def eqProductBody(x: Expr[Product], y: Expr[Product])(using Quotes): Expr[Boolean] = {
5353
elemInstances.zipWithIndex.foldLeft(Expr(true)) {
5454
case (acc, ('{ $elem: Eq[t] }, index)) =>
5555
val indexExpr = Expr(index)
5656
val e1 = '{ $x.productElement($indexExpr).asInstanceOf[t] }
5757
val e2 = '{ $y.productElement($indexExpr).asInstanceOf[t] }
5858
'{ $acc && $elem.eqv($e1, $e2) }
59-
}
59+
}
6060
}
61-
'{ eqProduct((x: T, y: T) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) }
61+
'{ eqProduct((x, y) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) }
6262

63-
// case for Mirror.ProductOf[T]
64-
// ...
63+
// case for Mirror.SumOf[T] ...
6564
```
6665

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:
66+
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.
67+
Being inside a macro, our first reaction would be to write the code below:
7268

7369
```scala
7470
'{
75-
summonAll[$m.MirroredElemTypes]
71+
summonInstances[T, $m.MirroredElemTypes]
7672
}
7773
```
7874

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

8278
```scala
8379
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} => ...
8480
```
8581

86-
Shown below is the implementation of `summonAll` as a macro. We assume that
87-
given instances for our primitive types exist.
88-
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-
```
82+
Shown below is the implementation of `summonInstances` as a macro, which for each type `elem` in the tuple type, calls
83+
`deriveOrSummon[T, elem]`.
9784

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:
85+
To understand `deriveOrSummon`, consider that if `elem` derives from the parent `T` type, then it is a recursive derivation.
86+
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.
10487

10588
```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
89+
def summonInstances[T: Type, Elems: Type](using Quotes): List[Expr[Eq[_]]] =
90+
Type.of[Elems] match
91+
case '[elem *: elems] => deriveOrSummon[T, elem] :: summonInstances[T, elems]
92+
case '[EmptyTuple] => Nil
11293

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.
94+
def deriveOrSummon[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
95+
Type.of[Elem] match
96+
case '[T] => deriveRec[T, Elem]
97+
case _ => '{ summonInline[Eq[Elem]] }
11798

118-
```scala
119-
extension [T](inline x: T)
120-
inline def === (inline y: T)(using eq: Eq[T]): Boolean = eq.eqv(x, y)
121-
122-
inline given eqGen[T]: Eq[T] = ${ Eq.derived[T] }
99+
def deriveRec[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
100+
import quotes.reflect.*
101+
Type.of[T] match
102+
case '[Elem] => report.errorAndAbort("infinite recursive derivation")
103+
case _ => derivedMacro[Elem] // recursive derivation
123104
```
124105

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:
106+
One additional difference with the body of `derivedMacro` 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.
107+
That is the rationale behind the `eqProductBody` function.
108+
Assuming that we calculate the equality of two `Person`s defined with a case class that holds a name of type [`String`](https://scala-lang.org/api/3.x/scala/Predef$.html#String-0) and an age of type `Int`, the equality check we want to generate is the following:
128109

129110
```scala
130-
case class Person(name: String, age: Int)
131-
132-
enum Opt[+T]:
133-
case Sm(t: T)
134-
case Nn
111+
true
112+
&& Eq[String].eqv(x.productElement(0),y.productElement(0))
113+
&& Eq[Int].eqv(x.productElement(1), y.productElement(1))
135114
```
136115

137116
The full code is shown below:
138117

139118
```scala
119+
import compiletime.summonInline
140120
import scala.deriving.*
141121
import scala.quoted.*
142122

143123

144124
trait Eq[T]:
145-
def eqv(x: T, y: T): Boolean
125+
def eqv(x: T, y: T): Boolean
146126

147127
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
128+
given Eq[String] with
129+
def eqv(x: String, y: String) = x == y
130+
131+
given Eq[Int] with
132+
def eqv(x: Int, y: Int) = x == y
133+
134+
def eqProduct[T](body: (T, T) => Boolean): Eq[T] =
135+
new Eq[T]:
136+
def eqv(x: T, y: T): Boolean = body(x, y)
137+
138+
def eqSum[T](body: (T, T) => Boolean): Eq[T] =
139+
new Eq[T]:
140+
def eqv(x: T, y: T): Boolean = body(x, y)
141+
142+
def summonInstances[T: Type, Elems: Type](using Quotes): List[Expr[Eq[_]]] =
143+
Type.of[Elems] match
144+
case '[elem *: elems] => deriveOrSummon[T, elem] :: summonInstances[T, elems]
145+
case '[EmptyTuple] => Nil
146+
147+
def deriveOrSummon[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
148+
Type.of[Elem] match
149+
case '[T] => deriveRec[T, Elem]
150+
case _ => '{ summonInline[Eq[Elem]] }
151+
152+
def deriveRec[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] =
153+
import quotes.reflect.*
154+
Type.of[T] match
155+
case '[Elem] => report.errorAndAbort("infinite recursive derivation")
156+
case _ => derivedMacro[Elem] // recursive derivation
157+
158+
inline def derived[T]: Eq[T] = ${ derivedMacro[T] }
159+
160+
def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] =
161+
import quotes.reflect.*
162+
163+
val ev: Expr[Mirror.Of[T]] = Expr.summon[Mirror.Of[T]].get
164+
165+
ev match
166+
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} =>
167+
val elemInstances = summonInstances[T, elementTypes]
168+
def eqProductBody(x: Expr[Product], y: Expr[Product])(using Quotes): Expr[Boolean] = {
169+
elemInstances.zipWithIndex.foldLeft(Expr(true)) {
170+
case (acc, ('{ $elem: Eq[t] }, index)) =>
171+
val indexExpr = Expr(index)
172+
val e1 = '{ $x.productElement($indexExpr).asInstanceOf[t] }
173+
val e2 = '{ $y.productElement($indexExpr).asInstanceOf[t] }
174+
'{ $acc && $elem.eqv($e1, $e2) }
175+
}
176+
}
177+
'{ eqProduct((x, y) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) }
178+
179+
case '{ $m: Mirror.SumOf[T] { type MirroredElemTypes = elementTypes }} =>
180+
val elemInstances = summonInstances[T, elementTypes]
181+
val elements = Expr.ofList(elemInstances)
182+
183+
def eqSumBody(x: Expr[T], y: Expr[T])(using Quotes): Expr[Boolean] =
184+
val ordx = '{ $m.ordinal($x) }
185+
val ordy = '{ $m.ordinal($y) }
186+
'{ $ordx == $ordy && $elements($ordx).asInstanceOf[Eq[Any]].eqv($x, $y) }
187+
188+
'{ eqSum((x, y) => ${eqSumBody('x, 'y)}) }
189+
end derivedMacro
198190
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] }
205191
```

0 commit comments

Comments
 (0)