0

I am trying to wrap a hierarchy of Java builders in a Kotlin type-safe builder. The hierarchy consists of the following builders (and their targets):

  • FigureBuilder (Figure)
  • LayoutBuilder (Layout)
  • TraceBuilder (Trace)

In Java, FigureBuilder has one method that takes a Layout, and another that take n traces, using a varargs method called addTraces():

addTraces(Trace... traces)

The assembly process in Java is basically

Figure f = Figure.builder()
   .layout(
       Layout.builder()
           .title("title")
           .build())
   .addTraces(
       ScatterTrace.builder()
           .name("my series")
           .build())
   .build();       

In Kotlin, I have code that creates the figure builder and the layout builder, but I am stuck on the trace builder. My code so far looks like this:

 val figure = figure {
            layout {title("Wins vs BA")}

            addTraces(
                ScatterTrace.builder(x, y)
                    .name("Batting avg.").build()
            )
        }


fun figure(c: FigureBuilder.() -> Unit) : Figure {
    val builder = Figure.builder()
    c(builder)
    return builder.build()
}

fun FigureBuilder.layout(c: Layout.LayoutBuilder.() -> Unit) {
    layout(Layout.builder().apply(c).build())
}

// won't compile: ScatterTrace.builder() requires 2 args
fun FigureBuilder.traces(vararg c: ScatterTrace.ScatterBuilder.() -> Unit) {
    c.forEach {
        val builder = ScatterTrace.builder()
        it(builder)
        addTraces(builder.build())
    }
}

I'm not at all sure the last function will work if I can get it to compile, but the immediate blocking issue is that ScatterTrace.builder() takes two arguments and I cannot figure out how to pass them into the lambda.

Thanks very much

L. Blanc
  • 2,150
  • 2
  • 21
  • 31
  • Have you tried to spread the ScatterTrace.ScatterBuilder args via spread operator (*)? – Brandon McAnsh Aug 18 '19 at 23:36
  • @BrandonMcAnsh. Sorry, I don't really know what that is. I will google it, but to be clear, IDK how to get even a single argument to a constructor inside the traces() method (or any other higher level function that takes a lambda-with-receivers as its argument. – L. Blanc Aug 19 '19 at 00:37

2 Answers2

1

It's strange that in Java you can create ScatterTrace.builder without arguments but in Kotlin you need two arguments to construct it. Maybe it will be better to apply traces one by one?

fun FigureBuilder.traces(x: Int, y: Int, c: ScatterTrace.ScatterBuilder.() -> Unit) {
    val builder = ScatterTrace.builder(x, y)
    c(builder)
    addTraces(builder.build())
}

val figure = figure {
    layout { title("Wins vs BA") }

    addTraces(
        trace(x, y) { name("Batting avg.") },
        trace(x, y) { name("Batting avg. 2") },
        trace(x, y) { name("Batting avg. 3") }
    )
}
Andrei Tanana
  • 7,932
  • 1
  • 27
  • 36
  • I can't do it in Java either. The issue is that the Java API has required arguments in the constructors for that class. The question behind the question is that if I want to do this kind of nice type-safe-builder in Kotlin for a class I don't control, am I limited to things with default (no arg) constructors? – L. Blanc Aug 19 '19 at 14:55
  • Anyway, you have to pass these constructor parameters. From my point of view, functions like `fun fun(builder parameters) { builder receiver lambda }` are good enough. – Andrei Tanana Aug 19 '19 at 17:22
  • This is a good solution. Combined with the spread operator suggestion from Brandon, it's got me 90% of where I want to be. The only issue is having to use parens for traces() rather than {}. It's an irregularity in my api that I would like to avoid, but I'll accept this as the answer if I can't come up with something better. I know other DSLs solve this somehow. kotlinx.html, for example, manages to put an arbitrary number of divs inside a body tag without wrapping the divs in parens. – L. Blanc Aug 19 '19 at 19:21
1
fun FigureBuilder.traces(vararg c: ScatterTrace.ScatterBuilder.() -> Unit) {
    addTraces(
            *c.map {
                val builder = ScatterTrace.builder()
                builder.build()
            }.toTypedArray()
    )
}

should do what you are looking for with your vararg requirement.

Brandon McAnsh
  • 992
  • 8
  • 18