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++ background then you'll be familiar with parametric and subtype polymorphism. However, Scala also supports ad-hoc 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 value of a type into a "String".

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

object Show:
    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) //the show method is available thanks to the subtyping relationships

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[Any] = List(E(), F())

//doesn't work. The reduction to the common sub-type has erased the necessary
// type information needed to power the type class usage.
list2.map(_.show)

With a type I'm calling "Container", it's possible 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](E()), Container[Show](F()))
list2.map(_.use(_.show))

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

The "use" method defined on the "Container" type makes using the data in the container relatively easy. The evidence of the type class is provided along with the contained data by the context function "A[B] ?=> B => C".

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

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