jedesah jedesah - 2 months ago 11
Scala Question

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?

Answer

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.

Comments