Scala: macro to create an instance from a class bo

2019-08-25 20:59发布

问题:

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.

回答1:

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")
    }
  }
}


回答2:

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.