Макрос 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 со странной дисперсией вызова сайта
- Возможно, вместо сеттеров есть другие методы, которые принимают несколько параметров в качестве переменных
- Возможно, есть перегруженные сеттеры с одинаковыми именами, но с разными типами аргументов.
- и т.п.
Я понимаю, что это не может быть решением, однако, я надеюсь, что это может быть полезно в качестве дополнительной проверки осуществимости, и что это может помочь сделать требования чуть-чуть более точными, прежде чем вкладывать больше времени и энергии в макробезопасный макрос решения.
Если это примерно так, как вы хотели, я мог бы обновить ответ. Если это не поможет, я удалю ответ.