Scala宏自动生成流畅的构建器

时间:2018-03-07 15:20:09

标签: scala builder scala-macros scala-macro-paradise scala-meta

我正在与外部Java API进行交互,如下所示:

val obj: SomeBigJavaObj = {
  val _obj = new SomeBigJavaObj(p1, p2)
  _obj.setFoo(p3)
  _obj.setBar(p4)
  val somethingElse = {
    val _obj2 = new SomethingElse(p5)
    _obj2.setBar(p6)
    _obj2
   }
  _obj.setSomethingElse(somethingElse)
  _obj
}

基本上,Java API公开了大量.setXXXX方法,这些方法返回void并设置了一些东西。我无法控制这些外部POJO。

因此,我想编写一个流畅的build Scala宏来检查对象,并为每个返回{{1}的.withXXXX()方法创建一个构建器模式类型void setXXXX()方法}}:

this

这可能吗?我知道我不能使用val obj: SomeBigJavaObj = build(new SomeBigJavaObj(p1, p2)) .withFoo(p3) .withBar(p4) .withSomethingElse( build(new SomethingElse(p5)) .withBar(p6) .result() ) .result() 宏生成新的顶级对象,所以我可以使用类似人机工程学的其他建议。

2 个答案:

答案 0 :(得分:5)

使用宏并不复杂;对IDE不友好(例如:代码完成; ......);

//编辑1:支持多个参数

实体:

public class Hello {
  public int    a;
  public String b;


  public void setA(int a) {
    this.a = a;
  }

  public void setB(String b) {
    this.b = b;
  }

  public void setAB(int a , String b){
    this.a = a;
    this.b = b;
  }
}

宏代码:     import scala.language.experimental.macros     import scala.reflect.macros.whitebox

trait BuildWrap[T] {
  def result(): T
}

object BuildWrap {
  def build[T](t: T): Any = macro BuildWrapImpl.impl[T]
}

class BuildWrapImpl(val c: whitebox.Context) {

  import c.universe._

  def impl[T: c.WeakTypeTag](t: c.Expr[T]) = {
    val tpe = c.weakTypeOf[T]
    //get all set member
    val setMembers = tpe.members
      .filter(_.isMethod)
      .filter(_.name.toString.startsWith("set"))
      .map(_.asMethod)
      .toList
    // temp value ;
    val valueName = TermName("valueName")

    val buildMethods = setMembers.map { member =>
      if (member.paramLists.length > 1)
        c.abort(c.enclosingPosition,"do not support Currying")

      val params = member.paramLists.head
      val paramsDef = params.map(e=>q"${e.name.toTermName} : ${e.typeSignature}")
      val paramsName = params.map(_.name)

      val fieldName = member.name.toString.drop(3)//drop set
      val buildFuncName = TermName(s"with$fieldName")
      q"def $buildFuncName(..$paramsDef ) = {$valueName.${member.name}(..$paramsName);this} "
    }


    val result =
      q"""new BuildWrap[$tpe] {
        private val $valueName = $t
        ..${buildMethods}
        def result() = $valueName

       }"""

    // debug
    println(showCode(result))
    result
  }
}

测试代码:

val hello1: Hello = BuildWrap.build(new Hello).withA(1).withB("b").result()
assert(hello1.a == 1)
assert(hello1.b == "b")

val hello2: Hello = BuildWrap.build(new Hello).withAB(1, "b").result()
assert(hello2.a == 1)
assert(hello2.b == "b")

答案 1 :(得分:0)

不是解决方案,只是一个非常初步的模拟

 +---------------------------------------------------------+
 |                                                         |
 |                 D I S C L A I M E R                     |
 |                                                         |
 |  This is a mock-up. It is not type-safe. It relies on   |
 |  runtime   reflection   (even  worse:  it  relies  on   |
 |  Java-reflection!). Do  not  use  this in production.   |
 |                                                         |
 |  If you can come up with a type-safe solution, I will   |
 |  definitely take a look at it and upvote your answer.   |
 |                                                         |
 +---------------------------------------------------------+

您已明确声明类型安全是必须的,因此下面的代码不能算作解决方案。但是,在进一步研究之前,您可能希望尝试纯粹基于运行时反射的实现,以更好地理解需求。这是一个非常快速和肮脏的模拟实现:

import scala.language.dynamics

class DynamicBuilder[X](underConstruction: X) extends Dynamic {
  val clazz = underConstruction.getClass
  def applyDynamic(name: String)(arg: Any): DynamicBuilder[X] = {
    if (name.startsWith("with")) {
      val propertyName = name.drop(4)
      val setterName = "set" + propertyName
      clazz.getDeclaredMethods().
        find(_.getName == setterName).
        fold(throw new IllegalArgumentException("No method " + setterName)) {
          m => 
          m.invoke(underConstruction, arg.asInstanceOf[java.lang.Object])
          this
        }
    } else {
      throw new IllegalArgumentException("Expected 'result' or 'withXYZ'")
    }
  }
  def result(): X = underConstruction
}

object DynamicBuilder {
  def build[A](a: A) = new DynamicBuilder[A](a)
}

导入build - 方法后

import DynamicBuilder.build

与POJO对应的类的定义在范围

class SomethingElse(val p5: String) {
  var bar: String = _
  def setBar(s: String): Unit = { bar = s }

  override def toString = s"SomethingElse[p5 = $p5, bar = $bar]"
}

class SomeBigJavaObj(val p1: Float, val p2: Double) {
  var foo: Int = 0
  var bar: String = _
  var sthElse: SomethingElse = _

  def setFoo(i: Int): Unit = { foo = i }
  def setBar(s: String): Unit = { bar = s }
  def setSomethingElse(s: SomethingElse): Unit = { sthElse = s }

  override def toString: String = 
    s"""|SomeBigJavaObj[
        |  p1 = $p1, p2 = $p2, 
        |  foo = $foo, bar = $bar, 
        |  sthElse = $sthElse
        |]""".stripMargin
}

以及您示例中的所有必需变量p1,...,p6都已定义

val p1 = 3.1415f
val p2 = 12345678d
val p3 = 42
val p4 = "BAR"
val p5 = "P5"
val p6 = "b-a-r"

您可以使用问题中的语法:

val obj: SomeBigJavaObj =
  build(new SomeBigJavaObj(p1, p2))
    .withFoo(p3)
    .withBar(p4)
    .withSomethingElse(
       build(new SomethingElse(p5))
         .withBar(p6)
         .result()
    )
    .result()

结果如下:

println(obj)
// Output:
// SomeBigJavaObj[
//   p1 = 3.1415, p2 = 1.2345678E7, 
//   foo = 42, bar = BAR, 
//   sthElse = SomethingElse[p5 = P5, bar = b-a-r]
// ]

目前,这个想法只是为了看看当你尝试使用一个更现实的例子时它失败了多么糟糕。事实证明,事实上,一切都有点复杂:

  • 也许有些setter是通用的
  • 也许其中一些人使用Java通配符和Java奇怪的呼叫站点差异
  • 也许有些其他方法将多个参数作为varargs
  • 而不是setter
  • 也许有重载的setter具有相同的名称但不同类型的参数。

我知道这不是一个解决方案,但是,我希望这可能有助于作为额外的可行性检查,并且在将更多时间和精力投入到类型中之前,它可能有助于使需求更加精确 - 安全的基于宏的解决方案。

如果这大致符合您的要求,我可能会考虑更新答案。如果这根本没用,我会删除答案。