Scala 宏自动生成流畅的构建器
Scala macro to auto generate fluent builders
我正在与外部 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
方法,这些方法 returns void
并设置了一些东西。我无法控制这些外部 POJO。
因此我想编写一个流畅的 build
Scala 宏来检查对象并为每个 void setXXXX()
方法创建一个构建器模式类型 .withXXXX()
方法 returns this
:
val obj: SomeBigJavaObj =
build(new SomeBigJavaObj(p1, p2))
.withFoo(p3)
.withBar(p4)
.withSomethingElse(
build(new SomethingElse(p5))
.withBar(p6)
.result()
)
.result()
这可能吗?我知道我无法使用 def
宏生成新的顶级对象,因此可以接受其他建议,我将拥有类似的人体工程学。
不是解决方案,只是非常初步的mock-up
+---------------------------------------------------------+
| |
| 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. |
| |
+---------------------------------------------------------+
你明确说了type-safety是必须的,所以下面的代码不能算作解决方案。但是,在进一步调查之前,您可能想尝试纯粹基于 runtime-reflection 的实现,以更好地理解需求。这是一个非常 quick-and-dirty mock-up 的实现:
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 的奇怪 call-site 方差
- 也许有一些其他方法可以代替 setter 将多个参数作为可变参数
- 可能存在名称相同但参数类型不同的重载 setter。
- 等等
我知道这不是解决方案,但是,我希望这可能对额外的可行性检查有用,并且可能有助于在投入更多时间和精力之前使要求更精确一点在 type-safe macro-based 个解决方案中。
如果这大致符合您的要求,我可能会考虑更新答案。如果这根本没有帮助,我会删除答案。
使用宏并不复杂;只是对 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")
我正在与外部 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
方法,这些方法 returns void
并设置了一些东西。我无法控制这些外部 POJO。
因此我想编写一个流畅的 build
Scala 宏来检查对象并为每个 void setXXXX()
方法创建一个构建器模式类型 .withXXXX()
方法 returns this
:
val obj: SomeBigJavaObj =
build(new SomeBigJavaObj(p1, p2))
.withFoo(p3)
.withBar(p4)
.withSomethingElse(
build(new SomethingElse(p5))
.withBar(p6)
.result()
)
.result()
这可能吗?我知道我无法使用 def
宏生成新的顶级对象,因此可以接受其他建议,我将拥有类似的人体工程学。
不是解决方案,只是非常初步的mock-up
+---------------------------------------------------------+
| |
| 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. |
| |
+---------------------------------------------------------+
你明确说了type-safety是必须的,所以下面的代码不能算作解决方案。但是,在进一步调查之前,您可能想尝试纯粹基于 runtime-reflection 的实现,以更好地理解需求。这是一个非常 quick-and-dirty mock-up 的实现:
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 的奇怪 call-site 方差
- 也许有一些其他方法可以代替 setter 将多个参数作为可变参数
- 可能存在名称相同但参数类型不同的重载 setter。
- 等等
我知道这不是解决方案,但是,我希望这可能对额外的可行性检查有用,并且可能有助于在投入更多时间和精力之前使要求更精确一点在 type-safe macro-based 个解决方案中。
如果这大致符合您的要求,我可能会考虑更新答案。如果这根本没有帮助,我会删除答案。
使用宏并不复杂;只是对 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")