Menu

Official website

Mastering Typeclass Derivation with Scala 3


07 Mar 2025

min read

Polymorphism

When learning about programming we stumble upon the concept of Polymorphism, and when learning something new I always wonder, why should we care? Besides getting a new tool on our tool belt I think it’s important to understand what are we getting from this new programming trick.

Polymorphism, from the Greek "having multiple forms", allows us to define and implement a set of different algorithms using the same interface. This has the benefit of lowering the complexity of our system from our client’s point of view. By switching to another implementation, they don’t need to understand what changed underneath, they just need to know that the interface is honored by another implementation. And by lowering this complexity we lower the cognitive-load developers need to have while using our API, making their code easier to maintain.

As far as I’m aware, there are at least 3 types of polymorphism:

  • Ad-Hoc

  • Parametric, and

  • Sub-type

If you come from a Object Oriented programming background you’ll recognize the last one. Sub-type polymorphism is implemented by defining a parent class that defines an API, and child classes implementing it. Then using a mechanism like double-dispatch, a implementation is chosen based on the actual subclass being instantiated:

trait Shape:
    def area: Double

class Square(width: Double) extends Shape:
    override def area: Double = width * width

class Rectangle(width: Double, height: Double) extends Shape:
    override def area: Double = width * height

class Circle(radius: Double) extends Shape:
    override def area: Double = Math.pow(radius, 2) * Math.PI

val s1: Shape = new Circle(4)
val s2: Shape = new Rectangle(4, 2)

s1.area
s2.area

Another type of polymorphism we can find in both Functional and Object Oriented programming languages is Parametric polymorphism where we don’t necessarily provide different implementations based on different types, but rather abstract over some types:

def identity[A](a: A): A = a

enum List[+A]:
    case Cons(head: A, tail: List[A]) extends List[A]
    case Nil extends List[Nothing]

    def map[B](fn: A => B): List[B] = this match
        case Cons(head, tail) => Cons(fn(head), tail.map(fn))
        case Nil => Nil

The List doesn’t care what’s containing, but its map method can easily work for `Int`s, `String`s, and so on.

The last type of polymorphism we mentioned was Ad-Hoc polymorphism, which can be defined as a "A system where functions or expressions are defined for specific types". Let’s see how this gets implemented using Typeclasses.

Typeclasses

Typeclasses are a way to implement Ad-Hoc polymorphism with 2 significant properties:

  • Separate Definitions: For each relevant class, we define a separate instance of a function or method (like Show, Eq, etc.) This similar to defining a separate function for specific types.

  • Context-Dependent Selection: An implementation will be selected based on where the typeclass is used.

In Scala, a Typeclass is implemented by defining 3 parts:

  • Interface: What functionality is the Typeclass offering.

  • Instances: How is that functionality implemented.

  • Usage: Where and how is that functionality used.

Let’s see a simple implementation of the Show Typeclass:

// Interface
trait Show[A]:
    def show(a: A): String

// Instances
object Show:
    given Show[Int]     = (i: Int) => i.toString
    given Show[String]  = (s: String) => "'" + s + "'"
    given Show[Boolean] = (b: Boolean) => i.toString

// Usage
def log[A](a: A)(using s: Show[A]): Unit =
    println(s.show(a))

An important detail of the Typeclass definition is that we use a Type Parameter on it’s interface, instances, and usage.

Typeclass Derivation

An interesting feature of Scala is that we can automatically generate the instances of a Typeclass by implementing Typeclass Derivation code. This will make the compiler generate instances at compile-time, making our Typeclass more usable as we’ll shift the burden of implementation to our code instead of our client’s code.

To derive instances for a Typeclass we need:

  • Tools to decompose complex types, such as case classes.

  • Tools to compose complex types from more simpler types.

And one way we can approach implementing Derivation code is:

  • Think Recursively (Base vs. Iterative Case)

  • Implementing one Typeclass by implementing a simpler version first.

On Scala 2 we’d use a library like Shapeless or Magnolia to compose and decompose complex types. In Scala 3 these features are backed in the language.

Mirrors provide typelevel information about types being derived, with similar features as HList and Coproduct in a single abstraction.

import scala.collection.AbstractIterable
import scala.compiletime.{erasedValue, error, summonInline}
import scala.deriving.*

// Interface
trait Show[A]:
    def show(a: A): String

// Instances
object Show:
    given Show[Int]     = (i: Int) => i.toString
    given Show[String]  = (s: String) => "'" + s + "'"
    given Show[Boolean] = (b: Boolean) => i.toString

    def iterable[T](p: T): Iterable[Any] = new AbstractIterable[Any]:
        def iterator: Iterator[Any] = p.asInstanceOf[Product].productIterator

    def showProduct[T](p: Mirror.ProductOf[T], elems: => List[Show[?]]): Show[T] =
        new Show[T]:
            def show(a: A): String =
                iterable(x).lazyZip(elems).map { case (i, s) => s.show(i) }.mkString(",")

    inline def derived[A](using m: Mirror.ProductOf[A]): Show[A] =
        lazy val elemInstances = summonInstances[T, m.MirroredElemTypes]
        showProduct(m, elemInstances)

    inline def summonInstances[T, Elems <: Tuple]: List[Show[?]] =
        inline erasedValue[Elems] match
            case _: (elem *: elems) => deriveOrSummon[T, elem] :: summonInstances[T, elems]
            case _: EmptyTuple => Nil

    inline def deriveOrSummon[T, Elem]: Show[Elem] =
        inline erasedValue[Elem] match
            case _: T => deriveRec[T, Elem]
            case _    => summonInline[Show[Elem]]

    inline def deriveRec[T, Elem]: Show[Elem] =
        inline erasedValue[T] match
            case _: Elem => error("infinite recursive derivation")
            case _       => Show.derived[Elem](using summonInline[Mirror.Of[Elem]]) // recursive derivation

This is a simplified example of a Typeclass Derivation for the Show Typeclass. For a more thorough example with a more detailed explanation, I cannot recommend Type Class Derivation enough.

expand_less