Search code examples
scalatypeclassshapelesstype-level-computation

What is the purpose of the emptyCoproduct and coproduct methods of the TypeClass trait in Shapeless


It is not fully clear to me what is the purpose of the emptyCoProduct and coproduct methods of the TypeClass trait in Shapeless.

When would one use the TypeClass trait instead of the ProductTypeClass?

What are some examples of ways those two methods would be implemented?


Solution

  • Suppose I've got a simple type class:

    trait Weight[A] { def apply(a: A): Int }
    
    object Weight {
      def apply[A](f: A => Int) = new Weight[A] { def apply(a: A) = f(a) }
    }
    

    And some instances:

    implicit val stringWeight: Weight[String] = Weight(_.size)
    implicit def intWeight: Weight[Int] = Weight(identity)
    

    And a case class:

    case class Foo(i: Int, s: String)
    

    And an ADT:

    sealed trait Root
    case class Bar(i: Int) extends Root
    case class Baz(s: String) extends Root
    

    I can define a ProductTypeClass instance for my type class:

    import shapeless._
    
    implicit object WeightTypeClass extends ProductTypeClass[Weight] {
      def emptyProduct: Weight[HNil] = Weight(_ => 0)
      def product[H, T <: HList](hw: Weight[H], tw: Weight[T]): Weight[H :: T] =
        Weight { case (h :: t) => hw(h) + tw(t) }
      def project[F, G](w: => Weight[G], to: F => G, from: G => F): Weight[F] =
        Weight(f => w(to(f)))
    }
    

    And use it like this:

    scala> object WeightHelper extends ProductTypeClassCompanion[Weight]
    defined object WeightHelper
    
    scala> import WeightHelper.auto._
    import WeightHelper.auto._
    
    scala> implicitly[Weight[Foo]]
    res0: Weight[Foo] = Weight$$anon$1@4daf1b4d
    
    scala> implicitly[Weight[Bar]]
    res1: Weight[Bar] = Weight$$anon$1@1cb152bb
    
    scala> implicitly[Weight[Baz]]
    res2: Weight[Baz] = Weight$$anon$1@74930887
    

    But!

    scala> implicitly[Weight[Root]]
    <console>:21: error: could not find implicit value for parameter e: Weight[Root]
                  implicitly[Weight[Root]]
                            ^
    

    This is a problem—it makes our automated type class instance derivation pretty much useless for ADTs. Fortunately we can use TypeClass instead:

    implicit object WeightTypeClass extends TypeClass[Weight] {
      def emptyProduct: Weight[HNil] = Weight(_ => 0)
      def product[H, T <: HList](hw: Weight[H], tw: Weight[T]): Weight[H :: T] =
        Weight { case (h :: t) => hw(h) + tw(t) }
      def project[F, G](w: => Weight[G], to: F => G, from: G => F): Weight[F] =
        Weight(f => w(to(f)))
      def emptyCoproduct: Weight[CNil] = Weight(_ => 0)
      def coproduct[L, R <: Coproduct]
        (lw: => Weight[L], rw: => Weight[R]): Weight[L :+: R] = Weight {
          case Inl(h) => lw(h)
          case Inr(t) => rw(t)
        }
    }
    

    And then:

    scala> object WeightHelper extends TypeClassCompanion[Weight]
    defined object WeightHelper
    
    scala> import WeightHelper.auto._
    import WeightHelper.auto._
    
    scala> implicitly[Weight[Root]]
    res0: Weight[Root] = Weight$$anon$1@7bc44e19
    

    All the other stuff above still works as well.

    To sum up: Shapeless's Coproduct is a kind of abstraction over ADTs, and in general you should provide instances of TypeClass for your type classes instead of just ProductTypeClass whenever possible.