Generic Type Class Derivation in Scala 3
source link: https://blog.oyanglul.us/scala/dotty/en/generic-type-class-derivation
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Generic Type Class Derivation in Scala 3
Table of Contents
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.
Source code 👉 https://github.com/jcouyang/meow
In the previous article Type Classes, we know it should be very straightforward to implement a Functor
instance, i.e. 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 s many data types to implement, and there are too many boilerplate for each of new data type, even though it is simple to do so.
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 generic represented as Product
or Coproduct
, in Scala 2 that is where shapeless come in handy,
and with some shapeless-based lib like Kittens, we can just directly derive these type class instances without actually implement them.
Example with Kittens:
import cats.derived._ implicit val showTree: Show[Tree] = semiauto.show implicit val functorTree: Functor[Tree] = semiauto.functor
In Scala 3, such thing is natively support and 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 build a generic type class derivation natively in Scala 3, the steps are pretty similar to how shapeless:
- Find generic representation of type
A
, that is, breakA
intoProduct
andCoproduct
combinations - implement instances for
Product
andCoproduct
types.
For the impatient, the final result of generic Show
derivation in Scala 3 will be 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 just 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]
, compiler will automatically derive the Mirror.Of[T]
type from T
, which will looks 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(...)
Don't need to memorize all of them for now, we will get to each of those types shortly.
Since Tree
is a Coproduct
(or Sum
) type, to show the data type Tree
correctly, we need to first figure out is it a Branch
or a Leaf
.
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 itsleft
andright
- if type is
Mirror.ProductOf[Leaf]
, recursively show itselem
Let's start with the simplest type, when 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 its 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 isLeaf
summonAsList
recursively findShow
instance 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 in 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 actually it is safe here, because we know that
T
is definitely a Product
so p
is Mirror.ProductOf[T]
.
The implementation should be capable to derived Show
for any type that has a Mirror.Product
instance. Let's give it a try:
assertEquals(show(Leaf("leaf")), """Leaf("leaf")""")
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 says if Tree
is constructed from Branch
, the ordinal is 0, and Leaf
's ordinal 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(", ")})"
Let us verify if this will work 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 are trying to do the same thing to generate generic Functor
instance, it won't just work.
Because if we look closer to 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 needs to pass in a type to return a type. For instance if F
is List
,
you will need to pass a A
to List
to get a type List[A]
. In another way, F
is just short 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 small trick(I borrowed from shapeless source code) to basically add [_] to original K0 Mirror.
Mirror { type MirroredType = T; type MirroredElemTypes <: Tuple }
That was pretty much it, and the rest of the implementation is pretty much the same as Show
.
I will just leave the implement of these ???
as exercise, feel free to look up the answer in source code of meow, send me a PR if you find a better implantation.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK