Use the lowest subtype in a typeclass?
Asked Answered
U

2

0

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!

Unutterable answered 23/4, 2020 at 17:2 Comment(0)
S
3

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

Suited answered 23/4, 2020 at 17:25 Comment(3)
In Magnolia 1.1.2 join is instead of combine, split is instead of dispatch scastie.scala-lang.org/DmytroMitin/05e9N9PLRba9htqVZGW1CA/1 libraryDependencies += "com.softwaremill.magnolia1_2" %% "magnolia" % "1.1.2" github.com/softwaremill/magnolia/tree/scala2 github.com/softwaremill/magnoliaSuited
github.com/propensive/wisteriaSuited
github.com/propensive/adversariaSuited
M
1

The most general solution is to just pack the typeclass instances in at the creation of myPets, existentially

final case class Packaged[+T, +P](wit: T, prf: P)
type WithInstance[T, +P[_ <: T]] = Packaged[U, P[U]] forSome { type U <: T }
implicit def packageInstance[T, U <: T, P[_ <: T]]
                            (wit: U)(implicit prf: P[U])
                          : T WithInstance P
= Packaged(wit, prf)

val myPets = List[Animal WithInstance Show](garfield, odie)
for(Packaged(p, showP) <- myPets) {
    implicit val showP1 = showP
    new Processor(p).print // note: should be def print()(implicit S: Show[A]), so that this can be .print()
}
Mercenary answered 23/4, 2020 at 18:6 Comment(2)
Nice approach. It's sad this will not work in Scala 3 (because of lack of existential types). Just curious, what do names wit and prf mean?Suited
@DmytroMitin witness and proof. You have some "real values" type T (a witness) along with some "proof" or "testimony" of some property P, such as Showability. This will work just fine in Scala 3 with some tweaks. sealed trait WithInstance[T, +P[_ <: T]] { type U <: T; val wit: U; val prf: P[U] }; object WithInstance { implicit def apply[T, V <: T, P[_ <: T]](wit0: V)(implicit prf0: P[V]): T WithInstance P = new WithInstance[T, P] { type U = V; val wit = wit0; val prf = prf0 }; def unapply[T, P[_ <: T]](wi: T WithInstance P): Some[(wi.U, P[wi.U])] = Some((wi.wit, wi.prf)) }Mercenary

© 2022 - 2024 — McMap. All rights reserved.