1
votes

I'm using two Scala libraries that both rely on implicit parameters to supply codecs/marshallers for case classes (the libraries in question are msgpack4s and op-rabbit). A simplified example follows:

sealed abstract trait Event
case class SomeEvent(msg: String) extends Event
case class OtherEvent(code: String) extends Event

// Assume library1 needs Show and library2 needs Printer

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

object ShowInstances {
  implicit val showSomeEvent = new Show[SomeEvent] {
    override def show(a: SomeEvent) =
      s"SomeEvent: ${a.msg}"
  }

  implicit val showOtherEvent = new Show[OtherEvent] {
    override def show(a: OtherEvent) =
      s"OtherEvent: ${a.code}"
  }
}

The Printer for the one library can be generic provided there's an implicit Show for the other library available:

object PrinterInstances {
  implicit def somePrinter[A: Show]: Printer[A] = new Printer[A] {
    override def printIt(a: A): Unit =
      println(implicitly[Show[A]].show(a))
  }
} 

I want to provide an API that abstracts over the details of the underlying libraries - callers should only need to pass the case class, internally to the API implementation the relevant implicits should be summoned.

object EventHandler {

  private def printEvent[A <: Event](a: A)(implicit printer: Printer[A]): Unit = {
    print("Handling event: ")
    printer.printIt(a)
  }

  def handle(a: Event): Unit = {
    import ShowInstances._
    import PrinterInstances._

    // I'd like to do this:
    //EventHandler.printEvent(a)

    // but I have to do this
    a match {
      case s: SomeEvent => EventHandler.printEvent(s)
      case o: OtherEvent => EventHandler.printEvent(o)
    }
  }
}

The comments in EventHandler.handle() method indicate my issue - is there a way to have the compiler select the right implicits for me?.

I suspect the answer is no because at compile time the compiler doesn't know which subclass of Event handle() will receive, but I wanted to see if there's another way. In my actual code, I control & can change the PrinterInstances code, but I can't change the signature of the printEvent method (that's provided by one of the libraries)

*EDIT: I think this is the same as Provide implicits for all subtypes of sealed type. The answer there is nearly 2 years old, I'm wondering if it's still the best approach?

1
Brute force approach: replace polymorphic implicit def somePrinter with a single implicit def somePrinter: Printer[Event] and handle cases inside.yeputons
Our situation is not quite as hopeless as Provide implicits for all subtypes of sealed type. In particular, we won't need the raw power of 'shapeless' :-)Fried Brice

1 Answers

2
votes

You have to do the pattern matching somewhere. Do it in the Show instance:

implicit val showEvent = new Show[Event] {
  def show(a: Event) = a match {
    case SomeEvent(msg) => s"SomeEvent: $msg"
    case OtherEvent(code) => s"OtherEvent: $code"
  }
}

If you absolutely need individual instances for SomeEvent and OtherEvent, you can provide them in a different object so they can be imported separately.

If Show is defined to be contravariant (i.e. as trait Show[-A] { ... }, with a minus on the generic type) then everything works out of the box and a Show[Event] is usable as a Show[SomeEvent] (and as a Show[OtherEvent] for that matter).

If Show is unfortunately not written to be contravariant, then we might have to do a little bit more juggling on our end than we'd like. One thing we can do is declare all of our SomeEvent values as simply Events, vis a vis val fooEvent: Event = SomeEvent("foo"). Then fooEvent will be showable.

In a more extreme version of the above trick, we can actually hide our inheritance hierarchy:

sealed trait Event {
  def fold[X]( withSomeEvent: String => X,
               withOtherEvent: String => X ): X
}

object Event {
  private case class SomeEvent(msg: String) extends Event {
    def fold[X]( withSomeEvent: String => X,
                 withOtherEvent: String => X ): X = withSomeEvent(msg)
  }
  private case class OtherEvent(code: String) extends Event {
    def fold[X]( withSomeEvent: String => X,
                 withOtherEvent: String => X ): X = withOtherEvent(code)
  }

  def someEvent(msg: String): Event = SomeEvent(msg)
  def otherEvent(code: String): Event = OtherEvent(code)
}

Event.someEvent and Event.otherEvent allow us to construct values, and fold allows us to pattern match.