Code Generation With KotlinPoet

KotlinPoet provides a simple API for generating Kotlin source files. It allows you to automate what would otherwise be tedious and repetitive tasks.

A Warning - Here Be Dragons

If you are neither a framework author nor a creator of coding tools it's probably best to close this tab. Forget you ever heard of this blog and go do something else instead. I assure you it's for your own good. Just look right here...

Neuralizer from Men In Black

The Perils Of Code Generation

Deciding to write a code generator is a bit like deciding to write your own ORM. It's never a good idea. Just like meta-programming it's a level of complexity and abstraction that is almost never required in regular application code. That being said it can be tremendous fun to write code that writes code, and also an opportunity to nurture your inner (and possibly outer) geek. For example I fondly remember working on a problem involving document management and discovering the best solution was to write an XSLT Stylesheet that generated XSLT Stylesheets. Plus of course Lisp fans always contend that homoiconicity is the most important characteristic of a programming language. So let's gird our loins and dive in!

Introducing KotlinPoet

KotlinPoet is a Kotlin library for generating Koltin code. It is based on JavaPoet but takes advantage of the many improvements and extra features Kotlin provides.

Here's a simple example, which produces an Employee class

fun main() {
    FileSpec.builder("", "KotlinPoetDemo")
        .addType(employeeType())
        .build()
        .writeTo(System.out)
}

This is the output from the program

class Employee(
  val name: String,
  val age: Int,
  var salary: Double) {

  init {
    println("""Created $name""")
  }

  fun awardBonus() {
    if (age > 40) {
        salary += 5000.0
    } else {
        salary += 3000.0
    }
  }

  override fun toString(): String = """$name of age $age earning $salary"""
}

Examining The KotlinPoet API

Most of the KotlinPoet API is based around builder objects and method chaining. Model classes (with names ending in 'Spec') are provided for the constructs of the Kotlin language, including classes, functions, properties and annotations. In the example below we use the TypeSpec model and fluent API to define the Employee class as having three properties, a primary constructor and two methods. KotlinPoet assumes that we want the properties to be initialised via the constructor.

private fun employeeType(): TypeSpec {
    return TypeSpec.classBuilder("Employee")
        .addProperty(employeeProperty("name", String::class))
        .addProperty(employeeProperty("age", Int::class))
        .addProperty(employeeProperty("salary", Double::class, true))
        .primaryConstructor(employeeConstructor())
        .addFunction(awardBonusFun())
        .addFunction(toStringFun())
        .build()
}

Here is the code for defining a property. Note KClass is the Kotlin equivalent of java.lang.Class and that we are taking advantage of default parameter values.

private fun employeeProperty(name: String, type: KClass<*>, isMutable: Boolean = false): PropertySpec {
    val spec = PropertySpec.builder(name, type).initializer(name)
    if(isMutable) {
        spec.mutable()
    }
    return spec.build()
}

Similarly here is the code for defining the constructor. Note we are trying to add a statement to what we defined above as a Primary Constructor. This forces KotlinPoet to insert an init block within the class body.

private fun employeeConstructor(): FunSpec {
    val msg = "Created \$name";
    return FunSpec.constructorBuilder()
        .addParameter("name", String::class)
        .addParameter("age", Int::class)
        .addParameter("salary", Double::class)
        .addStatement("println(%P)", msg)
        .build()
}

The method to generate the toString is self-explanatory, with the exception of the %P placeholder inside addStatement. This tells KotlinPoet not to escape any dollar symbols it finds inside msg, because we are deliberately trying to insert a string template into the generated code. If we had used the %S placeholder then the output would have been "${'$'}name of age ${'$'}age earning ${'$'}salary"

private fun toStringFun(): FunSpec {
    val msg = "\$name of age \$age earning \$salary"
    return FunSpec.builder("toString")
        .addModifiers(KModifier.OVERRIDE)
        .addStatement("return %P", msg)
        .returns(String::class)
        .build()
}

Finally here is the code to generate the awardBonus method. This illustrates one of the most convenient features of KotlinPoet, namely that the code to be inserted into functions is specified as strings. Typically we use triple-quoted strings to get much better readibility than the JavaPoet equivalent.

private fun awardBonusFun(): FunSpec {
    return FunSpec.builder("awardBonus")
        .addCode(
            """
                    |if (age > 40) {
                    |    salary += 5000.0
                    |} else {
                    |    salary += 3000.0
                    |}
                    |""".trimMargin())
        .build()
}

In other code generation libraries the low level details of the Abstract Syntax Tree (AST) are exposed to us. Typically there are node types for conditionals, iterations, local variable declarations, assignments etc... and it is up to us to create a graph that represents the syntax we had in mind. This is quite a challenge for even simple procedural code. The KotlinPoet alternative is elementary by comparison, although it does restrict us to code generation rather than transformation. So if your task was to write a refactoring tool you would have to use something else.

Conclusion

KotlinPoet is an extremely well designed library that brings code generation within the reach of mainstream developers. The typical enterprise developer should (hopefully) have no use for it, and those extending compilers and IDEs will need something more low level. But for everyone in between it's a great choice.

Article By
blog author

Garth Gilmour

Head of Learning