Scala reflection: how do I define a case class at runtime and then reference to it?

Viewed 189

I would like to define a case class at run-time, like for example

val tb = universe.runtimeMirror(getClass.getClassLoader).mkToolBox()
val myClass: ClassDef = q"case class Authentication(email: String)".asInstanceOf[ClassDef]
val definedClass  = tb.define(myClass)

And then being able to refer to it in another reflection

  // Actor code that recognise the defined case object
  val actorCode = q"""
  import akka.actor._
  object HelloActor {
    def props() = Props(new HelloActor())
  }
  class HelloActor() extends Actor {
    def receive = {
      case $definedClass(emailParam)  => println("case object instance has been received!")
      case _       => println("None received!")
    }
  }
  return HelloActor.props()
  """

Any idea on how to do the trick ?

2 Answers

The example that you shown is doable without any compile-time reflection:

import akka.actor._

// define extractor object: https://docs.scala-lang.org/tour/extractor-objects.html
sealed trait EmailExtractor[A] {
  def unapply(value: A): Option[String]
}
object EmailExtractor {
  
  def of[A](pf: PartialFunction[A, String]) = new EmailExtractor[A] {
    def unapply(value: A): Option[String] = pf.lift(value)
  }
}

// inject difference in behavior via constructor
class HelloActor[A](emailExtractor: EmailExtractor[A]) extends Actor {

  def receive = {
    case emailExtractor(emailParam) =>
      println("case class instance has been received!")
    case _ => println("None received!")
  }
}
object HelloActor {
  
  def props[A](emailExtractor: EmailExtractor[A]) =
    Props(new HelloActor(emailExtractor))
}

implicit val actorSystem: ActorSystem = ActorSystem()

// you'd have to define message somewhere available to both:
//  * actor you are creating
//  * place where you are sending a message
// anyway, there is no reason to generate a whole new actor because of it

def oneTimeCaseClass1 = {
  case class Message(email: String)
  
  actorSystem.actorOf(HelloActor.props(EmailExtractor.of[Message] {
    case Message(string) => string
  })) ! Message("test@test.com")
}

def oneTimeCaseClass2 = {
  case class Message(email: String)
  
  actorSystem.actorOf(HelloActor.props(EmailExtractor.of[Message] {
    case Message(string) => string
  })) ! Message("test2@test2.com")
}

oneTimeCaseClass1
oneTimeCaseClass2

scala.concurrent.Await.result(
  actorSystem.terminate(),
  scala.concurrent.duration.Duration.Inf
)

(See scastie)

Additionally:

  • return is a bad code practice
  • Akka Classic (untyped) is discouraged in favor of Akka Typed
  • the value of compile-time reflection lies exactly in the fact that it happens in the compile time - quasiquotes cannot be used in runtime to define new code, that requires a full-fledged compiler combined with a specific way of feeding its output to class loader (so basically embedding REPL) - this is dangerous (it's basically an arbitrary code execution), prone to OOM (because you cannot collect memory if REPL is storing its whole history in memory) and needless

Your code seems almost ok.

You just missed .companion in

val actorCode = q"""
   import akka.actor._
   object HelloActor {
     def props() = Props(new HelloActor())
   }
   class HelloActor() extends Actor {
     def receive = {
       case ${definedClass.companion}(emailParam)  => println("case object instance has been received!")
       case _       => println("None received!")
     }
   }
   ActorSystem("hellokernel").actorOf(HelloActor.props()) ! ${definedClass.companion}("abc")
 """

tb.eval(actorCode) // case object instance has been received!

Scala 2.13.8

Related