Skip to content
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

Add macro-less typeclass derivation example #7

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ To run an example:
- [abstractTypeclassBody](https://github.com/anatoliykmetyuk/dotty-macro-examples/tree/master/abstractTypeclassBody) – how to abstract a body of a function inside a macro-generated class into a separate macro.
- [accessMembersByName](https://github.com/anatoliykmetyuk/dotty-macro-examples/tree/master/accessMembersByName) – access an arbitrary member of a value given this member's name as a `String`.
- [defaultParamsInference](https://github.com/anatoliykmetyuk/dotty-macro-examples/tree/master/defaultParamsInference) – given a case class with default parameters, obtain the values of these default parameters.
- [typeclassDerivation](https://github.com/anatoliykmetyuk/dotty-macro-examples/tree/master/typeclassDerivation) - typeclass derivation.
- [macroTypeclassDerivation](https://github.com/anatoliykmetyuk/dotty-macro-examples/tree/master/macroTypeclassDerivation) – typeclass construction done with TASTy Reflect.
- [fullClassName](https://github.com/anatoliykmetyuk/dotty-macro-examples/tree/master/fullClassName) - get a fully qualified name of a class.
- [isMemberOfSealedTraitHierarchy](https://github.com/anatoliykmetyuk/dotty-macro-examples/tree/master/isMemberOfSealedTraitHierarchy) - check if a class inherits from a sealed trait.
Expand Down
4 changes: 3 additions & 1 deletion build.sc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import mill._, scalalib._

trait DottyModule extends ScalaModule {
def scalaVersion = "0.25.0-RC1"
def scalaVersion = "0.25.0-RC2"
}

object abstractTypeclassBody extends DottyModule
Expand All @@ -10,6 +10,7 @@ object defaultParamsInference extends DottyModule
object macroTypeclassDerivation extends DottyModule
object fullClassName extends DottyModule
object isMemberOfSealedTraitHierarchy extends DottyModule
object typeclassDerivation extends DottyModule

object test extends Module {
def all = List(
Expand All @@ -19,6 +20,7 @@ object test extends Module {
macroTypeclassDerivation,
fullClassName,
isMemberOfSealedTraitHierarchy,
typeclassDerivation,
)

def run = T {
Expand Down
2 changes: 1 addition & 1 deletion macroTypeclassDerivation/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
The objective is to implement a macro-powered typeclass derivation for sum and product types. We want to derive the Show type class. Please note that you can probably do the same with https://dotty.epfl.ch/docs/reference/contextual/derivation.html. This example is to illustrate the alternative approach involving macros that may be suitable for more advanced use cases.
The objective is to implement a macro-powered typeclass derivation for sum and product types. We want to derive the Show type class. Please note that you can do the same with https://dotty.epfl.ch/docs/reference/contextual/derivation.html – see [example](https://github.com/anatoliykmetyuk/dotty-macro-examples/tree/master/typeclassDerivation). This example is to illustrate the alternative approach involving macros that may be suitable for more advanced use cases.

First, we look at the type for which the macro is derived to see if it is a case class or a sealed trait. The derivation for each of these cases is then handled separately.

Expand Down
30 changes: 30 additions & 0 deletions typeclassDerivation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
The objective of this example is to derive the `Show` typeclass without using macros. To do so, we need to do three methods:

- Implement typeclass derivation for product types (case classes).
- Implement typeclass derivation for sum types (traits).
- Implement the `derives` method that will be able to derive the typeclass for both products and sums by delegation to the previously defined two methods.

Before walking through this tutorial, it is highly advisable to look through the [typeclass derivation documentation](https://dotty.epfl.ch/docs/reference/contextual/derivation.html).

# Product derivation
To display a case class `T`, we need to be able to display each field of the case class. Hence we need to obtain the `Show` typeclass for each field's type. We can obtain the fields' types by accessing the `m.MirroredElemTypes` where `m` is the `Mirror.ProductOf[T]`.

`m.MirroredElemTypes` gives us the tuple of all the fields' types – from this tuple we need to obtain a collection of `Show` typeclasses for each field's type. The following snipped does this: `summonAll[Tuple.Map[m.MirroredElemTypes, Show]]`. `summonAll` here accepts a tuple as a type argument and summons each type member of that tuple. `Tuple.Map` converts a tuple of field types into the tuple of typeclass types. For example, if for `Cat` the `m.MirroredElemTypes` is `String *: Int *: EmptyTuple` than `Tuple.Map[m.MirroredElemTypes, Show]` will be `Show[String] *: Show[Int] *: EmptyTuple`.

Other two things we need to display a case class are:

- The name of the case class
- The names of its fields

The former can be obtained by calling `constValue[m.MirroredLabel]`. `m.MirroredLabel` is a `String` singleton type that represents the name of the case class. E.g. for `Cat`, it is `"Cat"`. Note that it is a type and not a value, so we must call `constValue` to convert it to a value.

The names of the fields can be obtained by calling `constValueTuple[m.MirroredElemLabels]`. Similarly to the name of the case class, `m.MirroredElemLabels` store the names of the fields as singleton types, and `constValueTuple` is used to convert a tuple of types to a tuple value.

# Sum derivation
To derive a typeclass for a trait, we need to be able to derive a typeclass for every subtype of that trait. Once that is done, we need to check which subtype we are dealing with and return a typeclass for that subtype.

For example, if we have `Cat <: Animal` and `Dog <: Animal` and the goal is to derive the `Show[Animal]`, we first derive the typeclasses for both `Cat` and `Dog`. Then, on runtime, we check the exact type of the value supplied to the `show` function and display it using its precise type's typeclass.

We can obtain the typeclasses for all the subtypes of a trait similarly to how we did it for all the fields of a case class: `summonAll[Tuple.Map[m.MirroredElemTypes, Show]]`. For a trait, `m.MirroredElemTypes` will return the types of all its children.

A mirror of a trait has also a method `ordinal` defined on it. Once called on a trait's child instance, it will return an ordinal nubmer of the child's type in the trait's list of childern. Knowing this ordinal, it is possible to obtain the correct typeclass from the list of typeclasses of all the children – the required typeclass will have the same id in the list as the ordinal of the child's type.
61 changes: 61 additions & 0 deletions typeclassDerivation/src/Test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import deriving._, compiletime._

trait Show[T]:
def (x: T) show: String

object Show:
inline def product[T](using m: Mirror.ProductOf[T]): Show[T] = new {
def (x: T) show: String =
val memberTypeclasses: List[Show[_]] =
summonAll[Tuple.Map[m.MirroredElemTypes, Show]]
.toList.asInstanceOf[List[Show[_]]]
val productLabel: String = constValue[m.MirroredLabel]
val memberLabels: List[String] = constValueTuple[m.MirroredElemLabels]
.toList.asInstanceOf[List[String]]

val members: List[_] =
x.asInstanceOf[Product].productIterator.toList
val shownMembers: List[String] =
members.zip(memberTypeclasses).zip(memberLabels).map {
case ((member, showTcl: Show[a]), label) =>
val representation: String =
showTcl.show(member.asInstanceOf[a])
s"$label = $representation"
}

s"$productLabel: ${shownMembers.mkString(", ")}"
end show
}

inline def sum[T](using m: Mirror.SumOf[T]): Show[T] = new {
def (x: T) show: String =
val ordinal: Int = m.ordinal(x)
val typeclass: Show[T] =
summonAll[Tuple.Map[m.MirroredElemTypes, Show]]
.toArray(ordinal).asInstanceOf[Show[T]]
typeclass.show(x)
end show
}

inline given derived[T](using m: Mirror.Of[T]) as Show[T] =
inline m match
case s: Mirror.SumOf[T] => sum(using s)
case p: Mirror.ProductOf[T] => product(using p)
end derived

given as Show[String] = identity
given as Show[Int] = _.toString
end Show

sealed trait Animal derives Show
case class Cat(name: String, age: Int) extends Animal
case class Dog(name: String, owner: String) extends Animal

@main def Test =
val cat: Cat = Cat("Tom", 3)
val dog: Dog = Dog("Ball", "Tom")
val animal: Animal = cat
println(cat.show)
println(dog.show)
println(animal.show)
end Test