Search code examples
kotlinlambdabuilderkotlin-dsl

Kotlin: higher-order function taking vararg lamba-with-receiver, where the receiver takes arguments


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


Solution

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