Generic Type Class Derivation in Scala 3
I'm recently migrating some libs and projects to Scala 3, I guess it would be very helpful to me or anyone interested to learn some new functional programming features that Scala 3 is bringing to us.
- Rank N Types
- FunctionK
- GADT
- Phantom Types
- Dependent Types
- "First Class" Types
- Type Classes
- Generic Type Class Derivation
Source code 👉 https://github.com/jcouyang/meow
From the previous article Type Classes, we know that it should be very straightforward to implement a Functor instance, e.g. for Option.
given Functor[Option] with
def fmap[A, B](f: A => B): Option[A] => Option[B] = (oa: Option[A]) => oa match
case Some(a) => Some(f(a))
case None => None
However, there are so many data types to implement, and there is too much boilerplate for each new data type, although it is easy to do so.
Let's say we have a new ADT Tree:
enum Tree[T] {
case Branch(left: Tree[T], right: Tree[T])
case Leaf(elem: T)
}
If we want this Tree to be map-able, show-able, we need to implement Functor and Show instance for Tree.
Since Tree is an ADT, which means it can be represented as a generic type Product or Coproduct, this is where shapeless come in handy in Scala 2,
and with some shapeless-based libs like Kittens, we can just simply derive these type class instances without actually implementing them.
Example in Kittens https://github.com/typelevel/kittens#derive-functor :
import cats.derived._
implicit val showTree: Show[Tree] = semiauto.show
implicit val functorTree: Functor[Tree] = semiauto.functor
In Scala 3, something like this is natively supported, it can be done without any lib.
Let's start with the simpler type class Show.
Kind 0 Type Class Derivation
Show is type class for A, since A is just a type, let us call A Kind 0 and A[_] Kind 1.
To create a generic type class derivation natively in Scala 3, the steps are pretty similar to shapeless:
- Find generic representation of type
A, that is, breakAintoProductandCoproductcombinations - Implement instances for
ProductandCoproducttypes.
For the impatient, the final result of generic Show derivation in Scala 3 will look like:
enum Tree[T] derives Show {
case Branch(left: Tree[T], right: Tree[T])
case Leaf(elem: T)
}
Yeah, it is that simple, just add derives Show to the data type.
Generic representation of A
Scala 3 is able to derive the generic representation of data types as Mirror[T] type
https://dotty.epfl.ch/docs/reference/contextual/derivation.html#types-supporting-derives-clauses
, in the same way shapeless' Generic[T] type did.
In our example, to derive the generic representation of Tree, we can simply define a derived function:
inline def derived[T](using m: Mirror.Of[T]): Show[T] =
inline m match
case s: Mirror.SumOf[T] => ???
case p: Mirror.ProductOf[T] => ???
By using m: Mirror.Of[T], the compiler will automatically derive the Mirror.Of[T] type from T, which will look like:
new Mirror.Sum:
type MirroredType = Tree
type MirroredElemTypes[T] = (Branch[T], Leaf[T])
type MirroredMonoType = Tree[_]
type MirroredLabel = "Tree"
type MirroredElemLabels = ("Branch", "Leaf")
def ordinal(x: MirroredMonoType): Int = x match
case _: Branch[_] => 0
case _: Leaf[_] => 1
new Mirror.Product:
type MirroredType = Branch
type MirroredElemTypes[T] = (Tree[T], Tree[T])
type MirroredMonoType = Branch[_]
type MirroredLabel = "Branch"
type MirroredElemLabels = ("left", "right")
def fromProduct(p: Product): MirroredMonoType =
new Branch(...)
new Mirror.Product:
type MirroredType = Leaf
type MirroredElemTypes[T] = Tuple1[T]
type MirroredMonoType = Leaf[_]
type MirroredLabel = "Leaf"
type MirroredElemLabels = Tuple1["elem"]
def fromProduct(p: Product): MirroredMonoType =
new Leaf(...)
You don't have to memorize them all, we will get to each of these types shortly.
Since Tree is a Coproduct (Sum) type, we first need to figure out if it is a Branch or a Leaf to correctly represent the Tree data type.
Hence, the process to properly show Tree:
- compiler derives
Mirror.Of[Tree], the result is aMirror.Sum - break into
Mirror.Sum, if type isMirror.ProductOf[Branch], recursively show itsleftandright - if type is
Mirror.ProductOf[Leaf], recursively show itselem
Let's start with the simplest type, if our data type is a Leaf, how do we show it?
Generic instance of Show[Product] type
There is a singleton type MirroredLabel = "Leaf" we can use to show, and for whose elem, we can recursively derive Show instances for MirroredElemTypes
inline def summonAsList[T <: Tuple, F[_]]: List[F[Any]] =
inline erasedValue[T] match
case _: EmptyTuple => Nil
case _: (t *: ts) => summonInline[F[t]].asInstanceOf[F[Any]] :: summonAsList[ts, F]
inline def derived[T](using m: Mirror.Of[T]): Show[T] =
lazy val name = constValue[m.MirroredLabel]
lazy val elemsShowInstances = summonAsList[m.MirroredElemTypes, Show]
inline m match
case s: Mirror.SumOf[T] => ???
case p: Mirror.ProductOf[T] => ???
constValue[m.MirroredLabel]will get the singleton type's value, which isLeafsummonAsListrecursively findShowinstance for each ofLeaf's elements
Now we know the name of current type Leaf, and elemsShowInstances of Leaf's elements, let's try to fill the ??? with the actual implementation of Show[Product]
inline def derived[T](using m: Mirror.Of[T]): Show[T] =
lazy val name = constValue[m.MirroredLabel]
lazy val elemsShowInstances = summonAsList[m.MirroredElemTypes, Show]
inline m match
case s: Mirror.SumOf[T] => ???
case p: Mirror.ProductOf[T] => new Show[T]: // <- (ProductOf)
def show(a: T): String =
val elems = elemsShowInstances.iterator
.zip(a.asInstanceOf[Product].prodIterator) // <- (asInstanceOf)
.map { _.show(_) }
s"${name}(${elems.mkString(", ")})"
a.asInstanceOf[Product] looks really dangerous, but it's actually safe here, because we know that
T is definitely a Product, so p must be a Mirror.ProductOf[T].
The implementation should be capable to derive Show for any type that has a Mirror.Product instance. Let's give it a try:
assertEquals(show(Leaf("leaf")), """Leaf("leaf")""")
But that doesn't work yet, because even we know how to show Product but, Leaf is one of the Tree Coproduct type.
Coproduct type has one more concept ordinal, if you have pay attention in the Mirror.Sum instance of Tree:
new Mirror.Sum:
type MirroredType = Tree
type MirroredElemTypes[T] = (Branch[T], Leaf[T])
type MirroredMonoType = Tree[_]
type MirroredLabel = "Tree"
type MirroredElemLabels = ("Branch", "Leaf")
def ordinal(x: MirroredMonoType): Int = x match
case _: Branch[_] => 0
case _: Leaf[_] => 1
That is, when Tree is constructed from Branch, the ordinal is 0, and the ordinal of Leaf's is 1.
By simply checking the ordinal of T, we can show T properly:
inline def derived[T](using m: Mirror.Of[T]): Show[T] =
lazy val name = constValue[m.MirroredLabel]
lazy val elemsShowInstances = summonAsList[m.MirroredElemTypes, Show]
inline m match
case s: Mirror.SumOf[T] => new Show[T]:
def show(a: T): String =
val ord = s.ordinal(a) // <- (Ordinal)
s"${insts(ord).asInstanceOf[Show[T]].show(a)}: ${name}"
case p: Mirror.ProductOf[T] => new Show[T]:
def show(a: T): String =
val elems = elemsShowInstances.iterator
.zip(a.asInstanceOf[Product].prodIterator)
.map { _.show(_) }
s"${name}(${elems.mkString(", ")})"
Finally, let's verify that works as expected:
val aTree: Tree[String] = Branch(Leaf("l0"), Branch(Leaf("l1"), Leaf("r1")))
assertEquals(show(aTree), """Branch(Leaf("l0"): Tree, Branch(Leaf("l1"): Tree, Leaf("r1"): Tree): Tree): Tree""")
Kind 1 Type Class Derivation
If we try to do the same thing to create a generic Functor instance, it won't just work.
Because if we take a look closer look at the type class Show[A] and Functor[F[_]], you will notice that A
in Show[A] is a simple type, or we can call it Kind 0 Type. But F in is higher kind, and we can call
it Kind 1 Type here, because it must pass in a type to return a type. For example if F is List,
you will need to pass a A to List to get a type List[A]. In other words, F is just a shortcut for [X] =>> F[X].
Type lambda: https://dotty.epfl.ch/docs/reference/new-types/type-lambdas.html
But, the structure of creating a generic Functor instance is pretty much the same as Show, except we need a
special trick of Mirror for Kind 1 type.
inline given genFunctor[F[_]](using m: K1[F]): Functor[F] =
lazy val functors = summonKindAsList[LiftP[Functor, m.MirroredElemTypes], Functor]
inline m match
case s: K1Sum[F] => ???
case p: K1Product[F] => ???
Where K1 is simply a alias of Mirror
type K1[F[_]] = Mirror { type MirroredType[X] = F[X] ; type MirroredElemTypes[_] <: Tuple }
type K1Sum[F[_]] = Mirror.Sum { type MirroredType[X] = F[X] ; type MirroredElemTypes[_] <: Tuple }
type K1Product[F[_]] = Mirror.Product { type MirroredType[X] = F[X] ; type MirroredElemTypes[_] <: Tuple }
Which is just a little trick(I borrowed from shapeless source code) to basically add [_] to original K0 Mirror.
Mirror { type MirroredType = T; type MirroredElemTypes <: Tuple }
That's about it, and the rest of the implementation is pretty much the same as Show.
I will just leave the implementation of these ??? as an exercise, feel free to look up the answer in source code of meow, send me a PR if you find a better implantation.