0

I have multiple hierarchy of Scala traits that can compose an object in many ways. Example:

trait MainA { someDefs... }
trait SubA1 extends MainA { someDefs... }
trait SubA2 extends MainA { someDefs... }

trait MainB { someDefs... }
trait SubB1 extends MainB { someDefs... }
trait SubB2 extends MainB { someDefs... }

The composition should look (abstractly) like this:

new ComposedTrait with MainA with MainB

What I want to do is to create a composed trait on runtime with the prompted desired sub-traits of each case. On regular Java I know there is a method in class Class to obtain the class that matches with the name (Class.forName("name") I think), but it doesn't work in this case. This is what should look like:

//Should yield a ComposedTrait with a SubA2 and SubB1 traits
new ComposedTrait with Class.forName(SubA2) with Class.forName(SubB1)

Is there any way of making this possible? Like an alternative to the Class.forName that works in this scenario of composition?

xandor19
  • 41
  • 5

1 Answers1

1

If you know parent names at compile time you can write a macro (see sbt settings for macro projects)

import scala.language.experimental.macros
import scala.reflect.macros.{blackbox, whitebox}

def makeInstance[A](parentNames: String*): A = macro makeInstanceImpl[A]

def makeInstanceImpl[A: c.WeakTypeTag](
  c: whitebox/*blackbox*/.Context
)(parentNames: c.Tree*): c.Tree = {
  import c.universe._

  def getValue(tree: Tree): String = tree match {
    case q"${name: String}" => name
  }
  // def getValue(tree: Tree): String = c.eval(c.Expr[String](c.untypecheck(tree)))

  val parents = parentNames.map(name => tq"${TypeName(getValue(name))}")
  q"new ${weakTypeOf[A]} with ..$parents"
}

Please notice that if you make a macro whitebox then it can return a value of more precise type than declared (A)

val instance = makeInstance[ComposedTrait]("SubA2", "SubA1")

// checking the type
instance: ComposedTrait with SubA2 with SubA1 // not just ComposedTrait

  //   scalacOptions += "-Ymacro-debug-lite"
//scalac: {
//  final class $anon extends ComposedTrait with SubA2 with SubA1 {
//    def <init>() = {
//      super.<init>();
//      ()
//    }
//  };
//  new $anon()
//}

Scala Reflection--Create Instance from class name string of this class type

If you know parent names at runtime you can use reflective toolbox (runtime compilation)

import scala.reflect.runtime
import scala.reflect.runtime.universe._
import scala.tools.reflect.ToolBox
val rm = runtime.currentMirror
val tb = rm.mkToolBox()

def makeInstance[A: TypeTag](parentNames: String*): A = {
  val parents = parentNames.map(name => tq"${TypeName(name)}")
  tb.eval(q"new ${typeOf[A]} with ..$parents").asInstanceOf[A]
}

makeInstance[ComposedTrait]("SubA2", "SubA1")

Please notice that in such case statically you can't have a value of more precise type than ComposedTrait based on runtime strings (parent names).

If you need to create ComposedTrait at runtime you can do this as well

val classSymbol = tb.define(q"trait ComposedTrait".asInstanceOf[ClassDef])

def makeInstance(parentNames: String*): Any = {
  val parents = parentNames.map(name => tq"${TypeName(name)}")
  tb.eval(q"new $classSymbol with ..$parents")
}

makeInstance("SubA2", "SubA1")
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • 1
    I'll just add that macros were largely reworked in Scala 3, thus depending on your use case, this might be a reason to work with Scala 3 directly. – Gaël J Oct 16 '22 at 07:15
  • @GaëlJ In Scala 3 creating classes/instances of classes based on strings (parent names) is quite unpleasant https://stackoverflow.com/questions/73852787/tq-equivalent-in-scala-3-macros https://stackoverflow.com/questions/68550985/method-override-with-scala-3-macros – Dmytro Mitin Oct 16 '22 at 21:23
  • I'm having troubles with macros setup. I followed the sbt configuracion guide creating a project and a macro subproject but: For first example I get: **not found: type SubA2** at compile time For both ToolBox examples I get this: **Exception in thread "main" scala.tools.reflect.ToolBoxError: reflective compilation has failed: not found: type SubA2 not found: type SubB1** as a runtime exception – xandor19 Oct 20 '22 at 00:21
  • 1
    @xandor19 I can't publish at Scastie because it doesn't support multiple files or subprojects. Here are gists (including project structure): https://gist.github.com/DmytroMitin/76006c1710492bf6494f03d5d32c4398 https://gist.github.com/DmytroMitin/f9a587a7119468e4ee87ffceff48c86f – Dmytro Mitin Oct 21 '22 at 01:27
  • @xandor19 Did you manage to make this work? – Dmytro Mitin Oct 25 '22 at 06:55
  • @xandor19 Do you have any updates? – Dmytro Mitin Nov 02 '22 at 03:56
  • 1
    Sorry, I was quite busy with the normal tasks of the faculty and could'nt make more time for this after the last attempts. I'm going to take the gists and try to build my code from them. I hope to have an update soon. Thanks @DmytroMitin – xandor19 Nov 02 '22 at 19:38