I am building a DSL in Scala, and for that, I need to store "instances" of a class (Parent
in this case), except these "instances" must be re-created several times at runtime. So instead I am storing "constructor functions" - a lambda that makes the instance.
consider the following code - imagine that userVar
can change at runtime and the updated value must be used when constructing the instances.
class Parent {
def func(param: Any): Unit = { ... }
}
class User {
def construct(constr: => Parent): ParentWrapper = { ... }
var userVar = 13
construct(new Parent {
func(1)
func(userVar)
}
construct(new Parent {
func(userVar)
}
}
A more natural way of expressing what I want would be this (using the above definitions):
class User {
var userVar = 13
object ObjectA extends Parent {
func(1)
func(userVar)
}
construct(ObjectA)
}
However, that seems impossible, given that ObjectA
is created immediately, and doesn't have a "constructor".
I am thinking, with some creative use of macros, I could instead do this:
class User {
var userVar = 13
constructMacro {
func(1)
func(userVar}
}
}
and have the constructMacro
convert the code to construct(new Parent {code block goes here})
.
How would I do that?
Or is there a better way to avoid the awkward construct(new Parent{...})
call? My requirement is that somewhere in the User
class a reference is stored that I can repeatedly call and get new instances of the Parent
definition that reflect new values used in their construction -- and the construct
call should ideally return a wrapper object for that reference.
Unfortunately macros will not help.
Macro annotations (which expand before type checking) can't annotate code blocks:
@constructMacro {
func(1)
func(userVar)
}
is illegal.
Def macros (which expand during type checking) have their arguments type checked before macros are expanded. So
constructMacro {
func(1)
func(userVar)
}
doesn't compile:
Error: not found: value func
func(1)
Error: not found: value func
func(userVar)
That's the reason why macro shapeless.test.illTyped
accepts a string rather than code block:
illTyped("""
val x: Int = "a"
""")
rather than
illTyped {
val x: Int = "a"
}
So the closest you can implement is
constructMacro("""
func(1)
func(userVar)
""")
def constructMacro(block: String): ParentWrapper = macro constructMacroImpl
def constructMacroImpl(c: blackbox.Context)(block: c.Tree): c.Tree = {
import c.universe._
val q"${blockStr: String}" = block
val block1 = c.parse(blockStr)
q"""construct(new Parent {
..$block1
})"""
}
You can annotate a variable
@constructMacro val x = {
func(1)
func(userVar)
}
// becomes
// val x: ParentWrapper = construct(new Parent {
// func(1)
// func(userVar)
// })
@compileTimeOnly("enable macro paradise to expand macro annotations")
class constructMacro extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro constructMacroImpl.impl
}
object constructMacroImpl {
def impl(c: whitebox.Context)(annottees: c.Tree*): c.Tree = {
import c.universe._
annottees match {
case q"$mods val $tname: $tpt = { ..$stats }" :: Nil =>
q"$mods val $tname: ParentWrapper = construct(new Parent { ..$stats })"
case _ =>
c.abort(c.enclosingPosition, "Not a val")
}
}
}
If I understood correctly, you just need to change object
to def
in the ObjectA
block:
class User {
var userVar = 13
def makeParent = new Parent {
func(1)
func(userVar)
}
construct(makeParent)
}
and it'll do what you want.