Search code examples
scalatypeclassderiving

Use the lowest subtype in a typeclass?


I have the following code:

sealed trait Animal
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal

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

class Processor[A](a: A) {
  def print(implicit S: Show[A]): Unit = println(S.show(a))
}

implicit val showCat: Show[Cat] = c => s"Cat=${c.name}"
implicit val showDog: Show[Dog] = d => s"Dog=${d.name}"

val garfield = Cat("Garfield")
val odie = Dog("Odie")

val myPets = List(garfield, odie)

for (p <- myPets) {
  val processor = new Processor(p)
  processor.print // THIS FAILS AT THE MOMENT
}

Does anyone know of a nice way to get that line processor.print working?

I can think of 2 solutions:

  1. pattern match the p in the for loop.
  2. create an instance of Show[Animal] and pattern match it against all its subtypes.

But I'm wondering if there's a better way of doing this.

Thanks in advance!


Solution

  • Compile error is

    could not find implicit value for parameter S: Show[Product with Animal with java.io.Serializable]
    

    You can make Animal extend Product and Serializable

    sealed trait Animal extends Product with Serializable
    

    https://typelevel.org/blog/2018/05/09/product-with-serializable.html

    Also instead of defining implicit Show[Animal] manually

    implicit val showAnimal: Show[Animal] = {
      case x: Cat => implicitly[Show[Cat]].show(x)
      case x: Dog => implicitly[Show[Dog]].show(x)
      // ...
    }
    

    you can derive Show for sealed traits (having instances for descendants) with macros

    def derive[A]: Show[A] = macro impl[A]
    
    def impl[A: c.WeakTypeTag](c: blackbox.Context): c.Tree = {
      import c.universe._
      val typA = weakTypeOf[A]
      val subclasses = typA.typeSymbol.asClass.knownDirectSubclasses
      val cases = subclasses.map{ subclass =>
        cq"x: $subclass => _root_.scala.Predef.implicitly[Show[$subclass]].show(x)"
      }
      q"""
        new Show[$typA] {
          def show(a: $typA): _root_.java.lang.String = a match {
            case ..$cases
          }
        }"""
    }
    
    implicit val showAnimal: Show[Animal] = derive[Animal]
    

    or Shapeless

    implicit val showCnil: Show[CNil] = _.impossible
    
    implicit def showCcons[H, T <: Coproduct](implicit
      hShow: Show[H],
      tShow: Show[T]
    ): Show[H :+: T] = _.eliminate(hShow.show, tShow.show)
      
    implicit def showGen[A, C <: Coproduct](implicit
      gen: Generic.Aux[A, C],
      show: Show[C]
    ): Show[A] = a => show.show(gen.to(a))
    

    or Magnolia

    object ShowDerivation {
      type Typeclass[T] = Show[T]
    
      def combine[T](ctx: CaseClass[Show, T]): Show[T] = null
    
      def dispatch[T](ctx: SealedTrait[Show, T]): Show[T] =
        value => ctx.dispatch(value) { sub =>
          sub.typeclass.show(sub.cast(value))
        }
    
      implicit def gen[T]: Show[T] = macro Magnolia.gen[T]
    }
    
    import ShowDerivation.gen
    

    or Scalaz-deriving

    @scalaz.annotation.deriving(Show)
    sealed trait Animal extends Product with Serializable
    
    object Show {
      implicit val showDeriving: Deriving[Show] = new Decidablez[Show] {
        override def dividez[Z, A <: TList, ShowA <: TList](tcs: Prod[ShowA])(
          g: Z => Prod[A]
        )(implicit
          ev: A PairedWith ShowA
        ): Show[Z] = null
    
        override def choosez[Z, A <: TList, ShowA <: TList](tcs: Prod[ShowA])(
          g: Z => Cop[A]
        )(implicit
          ev: A PairedWith ShowA
        ): Show[Z] = z => {
          val x = g(z).zip(tcs)
          x.b.value.show(x.a)
        }
      }
    }
    

    For cats.Show with Kittens you can write just

    implicit val showAnimal: Show[Animal] = cats.derived.semi.show
    

    The thing is that garfield and odie in List(garfield, odie) have the same type and it's Animal instead of Cat and Dog. If you don't want to define instance of type class for parent type you can use list-like structure preserving types of individual elements, HList garfield :: odie :: HNil.


    For comparison deriving type classes in Scala 3

    How to access parameter list of case class in a dotty macro