holbech
holbech

Reputation: 573

Scala: Using right type class instance for reflected/runtime instance

I have a simple trait

trait BusinessObject {}

and a simple type-class

trait Printer[T<:BusinessObject] { def print(instance:T): Unit }

In my code base I have a few hundred implementations of BusinessObject. Some are direct implementers, some implement sub-traits of BusinessObject, and some add various mixin traits using with. I have around 10 different special implementations of Printer (defined on the various sub-traits and mixins), and a low-priority generic fallback instance for any other BusinessObject and it works a charm.

I need to document all the implementations of BusinessObject in the code base, so I have used Scala's reflection mechanism to enumerate these, and now want to apply a Printer on each. The method signature of the reflection mechanism is

def enumerateBOs: Traversable[BusinessObject]

It returns one instance of each BusinessObject implementation. My problem is that at runtime there seems to be no way to get the right (specific) Printer for each object in this traversable.

I have tried summoning using .type like this:

enumerateBOs.head match { case bo => Printer[bo.type].print(bo) }

but I get the generic fallback Printer for every element.

Is there some way to do what I want to do? Or, if implicits really only are available at compile time, some way to list all implementers of BusinessObject at compile time?

Upvotes: 4

Views: 312

Answers (1)

Oleg Pyzhcov
Oleg Pyzhcov

Reputation: 7353

Implicits (as all generics in Scala) are really a compile-time mechanism and it's not possible to locate all implementations of a non-sealed trait at compile time.


That being said, it's not hard to run Scala compiler at runtime.

Get your dependencies:

libraryDependencies ++= Seq(
  "org.scala-lang" % "scala-reflect" % "2.12.3",
  "org.scala-lang" % "scala-compiler" % "2.12.3"
)

You only need a ToolBox object to get everything done - it parses a string, then compiles parsed tree to a function () => Any, which, when called, gives the result of an expression. The code does not have an access to surrounding context too, so all types have to be fully qualified or imported.

import scala.reflect.runtime._
import scala.tools.reflect.ToolBox
import scala.util.Try

def unsafeCompile[A](code: String): A = {
  val tb = currentMirror.mkToolBox()
  tb.compile(tb.parse(code))().asInstanceOf[A]
}

The above function throws exceptions and does not really check if the cast to A is a valid one, so you can get ClassCastExceptions in unknown places if not used correctly.

But now, getting instances at runtime is just a matter of a few LOCs:

enumerateBOs.map { obj =>
  Try {
    val f = unsafeCompile[Any => Unit](s"""
      import your.package_.with_.Printer
      // any additional imports for instances go there too

      implicitly[Printer[_root_.${obj.getClass.getCanonicalName}]].print _
    """)
    f(obj)
  }
}

I'm assuming you're not using anonymous classes - their getCanonicalName returns null and you'll need some fallback in that case. It's rather slow too.

Upvotes: 1

Related Questions