Макрос Scala для автоматического создания беглых строителей

Я взаимодействую с внешним API Java, который выглядит следующим образом:

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, который проверяет объект и создает тип шаблона Builder .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()

Это возможно? Я знаю, что не могу генерировать новые объекты верхнего уровня с def Макросы так открыты для других предложений, где у меня была бы похожая эргономика.

2 ответа

Решение

Использовать макросы несложно; просто недружелюбно к IDE (как: завершение кода;...);

// edit 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;
  }
}

код макроса: импорт scala.language.experimental.macros импорт 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")

Не решение, просто очень предварительный макет

 +---------------------------------------------------------+
 |                                                         |
 |                 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

и определения классов, которые соответствуют POJOs находятся в области видимости

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]
// ]

На данный момент идея состоит в том, чтобы просто увидеть, насколько сильно он терпит неудачу, когда вы пытаетесь использовать его с несколько более реалистичным примером. Может оказаться, что на самом деле все немного сложнее:

  • Может быть, некоторые сеттеры являются общими
  • Может быть, некоторые из них используют символы подстановки Java со странной дисперсией вызова сайта
  • Возможно, вместо сеттеров есть другие методы, которые принимают несколько параметров в качестве переменных
  • Возможно, есть перегруженные сеттеры с одинаковыми именами, но с разными типами аргументов.
  • и т.п.

Я понимаю, что это не может быть решением, однако, я надеюсь, что это может быть полезно в качестве дополнительной проверки осуществимости, и что это может помочь сделать требования чуть-чуть более точными, прежде чем вкладывать больше времени и энергии в макробезопасный макрос решения.

Если это примерно так, как вы хотели, я мог бы обновить ответ. Если это не поможет, я удалю ответ.

Другие вопросы по тегам