Skip to content

Fix #8007: Add regression and show type class derivation with macros #8011

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 24, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 233 additions & 0 deletions docs/docs/reference/contextual/derivation-macro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
---
layout: doc-page
title: How to write a type class `derived` method using macros
---

In the main [derivation](./derivation.md) documentation page we explaind the
details behind `Mirror`s 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.matching.summonExpr` 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:

```scala
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 an instance for `Eq[T]` given a `Mirror[T]`. Here is a possible
signature,

```scala
def derived[T: Type](ev: Expr[Mirror.Of[T]])(given qctx: QuoteContext): Expr[Eq[T]] = ???
```

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

```scala
inline given derived[T]: (m: 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. The body of this method is
shown below:


```scala
def derived[T: Type](m: Expr[Mirror.Of[T]])(given qctx: QuoteContext): Expr[Eq[T]] = {
import qctx.tasty.{_, given}

val elementTypes = m match {
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = $elem } } => elem
}

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)})
}
}
```

Note, that in the `inline` case we can merely write
`summonAll[m.MirroredElemTypes]` inside the inline method but here, since
`summonExpr` is required if we need to query the context we need to 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:

```scala
'{
summonAll[$m.MirroredElemTypes]
}
```

Instead we extract the tuple-type for element types using pattern matching over
quotes and more specifically of the refined type:

```scala
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = $elem } } => elem
```

The implementation of `summonAll` as a macro can be show below:

```scala
def summonAll[T](t: Type[T])(given qctx: QuoteContext): List[Expr[Eq[_]]] = t match {
case '[$tpe *: $tpes] => summonExpr(given '[Eq[$tpe]]).get :: summonAll(tpes)
case '[Unit] => Nil
}
```

Note, that in a realistic implementation the `summonExpr(given '[Eq[$tpe]]).get`
is going to fail if the necessary given instances for some type are not present.

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 `Person`s 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:

```scala
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](../metaprogramming.md) we create two methods.
One that hosts the top-level splice `eqv` and one that is the implementation.

```scala
inline def eqv[T](value: =>T, value2: =>T): Boolean = ${ eqvImpl('value, 'value2) }

def eqvImpl[T: Type](value: Expr[T], value2: Expr[T])(given qctx: QuoteContext): Expr[Boolean] = {
import qctx.tasty.{_, given}

val mirrorTpe = '[Mirror.Of[T]]
val mirrorExpr = summonExpr(given mirrorTpe).get
val derivedInstance = Eq.derived(mirrorExpr)

'{
$derivedInstance.eqv($value, $value2)
}
}
```

Note, that we need to quote the type we need `Mirror.Of[T]` with the quoted
syntax for types and then trigger its synthesis with `summonExpr`. `mirrorExpr`
now holds the refined type for e.g., a `Person`:

```scala
scala.deriving.Mirror {
type MirroredType >: Person <: Person
type MirroredMonoType >: Person <: Person
type MirroredElemTypes >: scala.Nothing <: scala.Tuple
} & scala.deriving.Mirror.Product {
type MirroredMonoType >: Person <: Person
type MirroredType >: Person <: Person
type MirroredLabel >: "Person" <: "Person"
} {
type MirroredElemTypes >: scala.*:[scala.Predef.String, scala.*:[scala.Int, scala.Unit]] <: scala.*:[scala.Predef.String, scala.*:[scala.Int, scala.Unit]]
type MirroredElemLabels >: scala.*:["name", scala.*:["age", scala.Unit]] <: scala.*:["name", scala.*:["age", scala.Unit]]
}
```

The derived instance then is finally generated with:

```scala
val derivedInstance = Eq.derived(mirrorExpr)

'{
$derivedInstance.eqv($value, $value2)
}
```

The full code is shown below:

```scala
import scala.deriving._
import scala.quoted._
import scala.quoted.matching._

object Macro {

trait Eq[T] {
def eqv(x: T, y: T): Boolean
}

object Eq {
given Eq[String] {
def eqv(x: String, y: String) = x == y
}

given Eq[Int] {
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 summonAll[T](t: Type[T])(given qctx: QuoteContext): List[Expr[Eq[_]]] = t match {
case '[$tpe *: $tpes] => summonExpr(given '[Eq[$tpe]]).get :: summonAll(tpes)
case '[Unit] => Nil
}

def derived[T: Type](ev: Expr[Mirror.Of[T]])(given qctx: QuoteContext): Expr[Eq[T]] = {
import qctx.tasty.{_, given}

val elementTypes = ev match {
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = $elem } } => elem
}

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)})
}
}
}

inline def eqv[T](value: =>T, value2: =>T): Boolean = ${ eqvImpl('value, 'value2) }

def eqvImpl[T: Type](value: Expr[T], value2: Expr[T])(given qctx: QuoteContext): Expr[Boolean] = {
import qctx.tasty.{_, given}

val mirrorTpe = '[Mirror.Of[T]]
val mirrorExpr = summonExpr(given mirrorTpe).get
val derivedInstance = Eq.derived(mirrorExpr)

'{
$derivedInstance.eqv($value, $value2)
}
}
}
```
4 changes: 4 additions & 0 deletions docs/docs/reference/contextual/derivation.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ inline def derived[A](given gen: K0.Generic[A]): Eq[A] = gen.derive(eqSum, eqPro

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

For a brief discussion on how to use macros to write a type class `derived`
method please read more at [How to write a type class `derived` method using
macros](./derivation-macro.md).

### Deriving instances elsewhere

Sometimes one would like to derive a type class instance for an ADT after the ADT is defined, without being able to
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/reference/metaprogramming/macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ sum
### Find implicits within a macro

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

```scala
inline def setFor[T]: Set[T] = ${ setForExpr[T] }
Expand Down
9 changes: 9 additions & 0 deletions tests/run-macros/i8007.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
List("name", "age")

Test 23
()

true

false

31 changes: 31 additions & 0 deletions tests/run-macros/i8007/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import scala.deriving._
import scala.quoted._
import scala.quoted.matching._

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

def mirrorFields[T](t: Type[T])(given qctx: QuoteContext): List[String] =
t match {
case '[$field *: $fields] => field.show :: mirrorFields(fields)
case '[Unit] => Nil
}

// Demonstrates the use of quoted pattern matching
// over a refined type extracting the tuple type
// for e.g., MirroredElemLabels
inline def test1[T](value: =>T): List[String] =
${ test1Impl('value) }

def test1Impl[T: Type](value: Expr[T])(given qctx: QuoteContext): Expr[List[String]] = {
import qctx.tasty.{_, given}

val mirrorTpe = '[Mirror.Of[T]]

summonExpr(given mirrorTpe).get match {
case '{ $m: Mirror.ProductOf[T]{ type MirroredElemLabels = $t } } => {
Expr(mirrorFields(t))
}
}
}
}
57 changes: 57 additions & 0 deletions tests/run-macros/i8007/Macro_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import scala.deriving._
import scala.quoted._
import scala.quoted.matching._

object Macro2 {

def mirrorFields[T](t: Type[T])(given qctx: QuoteContext): List[String] =
t match {
case '[$field *: $fields] => field.show.substring(1, field.show.length-1) :: mirrorFields(fields)
case '[Unit] => Nil
}

trait JsonEncoder[T] {
def encode(elem: T): String
}

object JsonEncoder {
def emitJsonEncoder[T](body: T => String): JsonEncoder[T]=
new JsonEncoder[T] {
def encode(elem: T): String = body(elem)
}

def derived[T: Type](ev: Expr[Mirror.Of[T]])(given qctx: QuoteContext): Expr[JsonEncoder[T]] = {
import qctx.tasty.{_, given}

val fields = ev match {
case '{ $m: Mirror.ProductOf[T] { type MirroredElemLabels = $t } } =>
mirrorFields(t)
}

val body: Expr[T] => Expr[String] = elem =>
fields.reverse.foldLeft(Expr("")){ (acc, field) =>
val res = Select.unique(elem.unseal, field).seal
'{ $res.toString + " " + $acc }
}

'{
emitJsonEncoder((x: T) => ${body('x)})
}
}
}

inline def test2[T](value: =>T): Unit = ${ test2Impl('value) }

def test2Impl[T: Type](value: Expr[T])(given qctx: QuoteContext): Expr[Unit] = {
import qctx.tasty.{_, given}

val mirrorTpe = '[Mirror.Of[T]]
val mirrorExpr = summonExpr(given mirrorTpe).get
val derivedInstance = JsonEncoder.derived(mirrorExpr)

'{
val res = $derivedInstance.encode($value)
println(res)
}
}
}
Loading