1

I am interacting with an external Java API which looks like this:

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
}

Basically the Java API exposes bunch of .setXXXX methods which returns void and sets something. I have no control over these external POJOs.

I would therefore like to write a fluent build Scala macro which inspects the object and creates a builder-pattern type .withXXXX() method for each of the void setXXXX() methods which returns this:

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

Is this possible? I know I cannot generate new top level objects with def macros so open to other suggestions where I would have the similar ergonomics.

pathikrit
  • 32,469
  • 37
  • 142
  • 221
  • Does it specifically have to be a macro that converts everything into plain-old method calls, or would a solution that uses some runtime reflection also be acceptable? Similar: is absolute type-safety a must here? – Andrey Tyukin Mar 09 '18 at 18:28
  • 1
    runtime reflection is fine. Type-safety is a must ofcourse. – pathikrit Mar 09 '18 at 18:45

2 Answers2

6

It is not complicated to use macros; just unfriendly to IDE (like:code completion;...);

//edit 1 : support multiple arguments

entity:

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

macro code : 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
  }
}

test code :

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,404
  • 1
  • 11
  • 14
0

Not a solution, just a very preliminary 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.   |
 |                                                         |
 +---------------------------------------------------------+

You have said explicitly that type-safety is a must, so the code below cannot count as a solution. However, before investigating any further, maybe you would like to experiment with a purely runtime-reflection based implementation, to understand the requirements better. Here is a very quick-and-dirty mock-up implementation:

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

Once the build-method is imported

import DynamicBuilder.build

and the definitions of the classes that correspond to POJOs are in scope

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
}

and also all the required variables p1,...,p6 from your example are defined

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

you can use exactly the syntax from your question:

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

The result looks as follows:

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

For now, the idea is just to see how badly it fails when you try to use it with a somewhat more realistic example. It might turn out that in reality, everything is a little bit more complicated:

  • Maybe some setters are generic
  • Maybe some of them use Java wildcards with Java's strange call-site variance
  • Maybe instead of setters there are some other methods that take multiple parameters as varargs
  • Maybe there are overloaded setters with same name but different types of arguments.
  • etc.

I understand that this cannot be a solution, however, I hope that this might be useful as an additional feasibility check, and that it maybe helps to make the requirements a tiny bit more precise, before investing more time and energy in type-safe macro-based solutions.

If this does roughly what you wanted, I might consider to update the answer. If this is not helpful at all, I'll delete the answer.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • This is cool but we threw static types out of the window right? – pathikrit Mar 12 '18 at 13:27
  • @pathikrit Right, static types mostly gone, preserved only in the return type of the builder. But if you think that otherwise this mock-up does exactly what you want, I *might* try to come up with a type-safe solution. If instead, you notice that it doesn't work in thousand different ways because of overloading, or varargs, or some wildcards, or java's weird call-site variance etc. etc. , then I will probably better delete this answer altogether. There are just too many things that could get in the way. So, what do you think, is it feasible or not? – Andrey Tyukin Mar 12 '18 at 14:43
  • Throwing static typing makes this useless for me unfortunately but do not delete the answer - I think it is useful. Put a disclaimer at the top. Thanks! – pathikrit Mar 12 '18 at 15:02
  • 1
    @pathikrit I've added a ridiculously huge disclaimer. Still, I would not want to embark upon a type-safe solution without getting any feedback about the mock-up. If it works perfecly, I would consider extending the answer. If it fails for all the reasons enumerated previously, I would deem it unfeasible and forget the whole thing. I just don't want to [fix everything later with a sledge hammer](https://en.wikipedia.org/wiki/Mockup). – Andrey Tyukin Mar 13 '18 at 02:02