Containers

Associating items by Type Class

When learning Scala you'll eventually encounter two forms of polymorphism. If you come from a Java or C++ then you'll be familiar with parametric and subtype polymorphism. However, Scala also supports ad-hoc polymorphism via type classes. A type class in Scala 3 is usually formed by defining a trait that will be implemented for the supporting types. A prime example of this is the Show type class, a type class that's used to turn a type into a String.

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

object Show:
  //implementation of Show for Int
  given Show[Int] with 
    def show(i: Int) = i.toString

  extension [A](a: A)(using s: Show[A]) def show = s.show(a)

Ad-hoc polymorphism in Scala ends up being more flexible than standard subtype polymorphism in a lot of ways, but one thing it's weak in is grouping items that are related by a type class. For example:

trait B:
  def show = "hi"
class C extends B  
class D extends B

val list: List[B] = List(C(), D())

list.map(_.show)

class E

object E:
  given Show[E] with 
    def show(e: E) = "E"

class F

object F: 
  given Show[F] with 
    def show(f: F) = "F"

val list2 = List(E(), F())

list2.map(???) // we want to use Show, but the evidence for the typeclass is lost by shoving both into a list

With a type I've named Container, it's possible however, to preserve the type class information for the types when grouping them together:

trait Container[A[_]]:
  type B
  val b: B
  given ev: A[B]
  def use[C](fn: A[B] ?=> B => C): C = fn(b)

object Container:
  inline def apply[A[_]](a: Any) = ${
    applyImpl[A]('a)
  }

  private def applyImpl[A[_]](a: Expr[Any])(using Quotes, Type[A]) =
    import quotes.reflect.*
    a match
      case '{ $a: i } =>
        TypeRepr.of[i].widen.asType match 
          case '[j] =>
            val expr = Expr
              .summon[A[j]]
              .getOrElse(
                report.errorAndAbort(
                  s"Can't find instance of ${Type.show[A[j]]} for ${Type.show[j]}"
                )
              )
            '{
              new Container[A]:
                type B = j
                val b: B = ${a.asExprOf[j]}
                given ev: A[j] = $expr
            }

val list2: List[Container[Show]] = List(Container[Show](E()), Container[Show](F()))
list2.map(_.use(_.show)) //List("E", "F")

This Container type maintains the information about the type class by storing the type class implementation in a trait, and by proving to the Scala compiler that the data contained in the container is compatible with the type class.

The use method located on the container type makes using the data in the container easy. The evidence of the type class is provided by the context function A[B] ?=> B => C.

A helper macro is provided in the companion object of Container in order to give a nicer syntax that doesn't require us to provide the soon-to-be erased type of the data that has a type class implementation.

Hope this helps anyone that wants to group data by type class! Happy Scala Hacking!!

Did you find this article valuable?

Support Mark Hammons by becoming a sponsor. Any amount is appreciated!