Creating Type Safe Builders In Kotlin


A Domain Driven Approach

Today we are going to cover creating Type Safe Builders. If you’ve heard of Kotlin’s DSL or Domain Specific Language capabilities this is what they are referring to.

A Type Safe Builder is really just that. It’s building an object in a particular domain. For example if you are working on code for a purchasing system then being able to write code tailored to that domain comes in handy. Especially if you have to do it frequently.

As android developers ANKO is probably the most notable example. Since our UI can and often does take a myriad of different combinations that we often have to craft by hand this makes for a perfect Type Safe Builder use case.

The other common example we see for Type Safe Builders is with generating html. The KotlinX.html library is an example of this. Since html has a standard format you can think of it as configuring the document.

Why you should read this?

When I was looking through the documentation and examples none of them covered how to go from an existing domain with models that had properties to creating a Type Safe Builder.

In particular all of the examples I saw were using zero argument models. That was not helpful as all of my models tend to not have zero argument constructors.

I’m going to cover TSB from a domain approach. I already had models defined and I wanted to create a Type Safe Builder for my domain.

But before I get into the details let’s take a look at an example of a Type Safe Builder is.

A quick note before we dive in. This is a super simple example, mostly for the purposes of making it easy to understand. In a real world scenario the domain that is being modeled would likely be more complicated making the rational for making a DSL more apparent.

These builders resemble JSON. For example this piece of JSON:

"person" : {
    "guid": "dd9e889a-a4a9-4930-9f1d-498e44a9cb96",
    "isActive": true,
    "picture": "http://placehold.it/32x32",
    "age": 21,
    "eyeColor": "green",
    "name": "Bridges Justice",
    "gender": "male",
    "email": "bridgesjustice@mangelica.com",
    "phone": "+1 (914) 555-2368",
    "address": "668 Dewey Place, Fillmore, Mississippi, 6828",
    "latitude": 50.761288,
    "longitude": -134.887256,
  }

Could be written as a Type Safe Builder using the DSL features of Kotlin. The above could look like the following:

person {
    guid = "dd9e889a-a4a9-4930-9f1d-498e44a9cb96"
    isActive = true
    picture = "http://placehold.it/32x32"
    age = 30
    eyeColor = "hazel"
    gender = "male"
    name {
        first = "Bridges"
        last = "Justice"    
    }
    contact {
        email = "bridgesjustice@mangelica.com"
        phone = "+1 (914) 555-2368"
        addresses {
             home = "668 Dewey Place, Fillmore, Mississippi, 6828"
             work = "100 Wallaby Way"
        }
    }

As you can see this is super easy to read. Especially if you compare it to the alternative. For example if you had to write this the imperative way of calling a constructor you’d have to write something like the following:

Person("dd9e889a-a4a9-4930-9f1d-498e44a9cb96", Name("Bridges", "Justice"), true, 
"http://placehold.it/32x32", 30, "hazel", "male", 
Contact("bridgesjustice@mangelica.com", "+1 (914) 555-2368", "668 Dewey Place, 
Fillmore, Mississippi, 6828",  50.761288, -134.887256))

I’d prefer the Type Safe Builder declarative way almost any day. But there are trade offs. We’ll touch on them a bit more later. For now let’s dive into how to create our Type Safe Builder.

Functions

The first thing to understand about Type Safe Builders is that they are just functions. The assignments in Person is just code in a function:

person {
    // arbitrary code goes here
    val computation = 5 * 2
}

Since this is just function it could actually be written as the following:

person( {
    // arbitrary code goes here
    val computation = 5 * 2
})

In Kotlin if the last argument of a function is a lambda expression the parentheses can be omitted.

Higher Order Functions

While they are just functions they also happen to have parameters with function types. As a result they actually called a Higher-Order Function.

A higher-order function is a function that takes functions as parameters, or returns a function.

In order to do that in Kotlin we have to define the person function. First we define the person function:

fun person(){}

Then we need to specify that it takes one argument.

fun person(function:

In particular it takes another function:

fun person(function:()->Unit) {}

We are specifically are saying that our function takes no arguments and returns nothing. If we wanted to specify that person function took a function that took an integer argument then we’d define it like so:

fun person(function:(Int)->Unit) {}

Likewise if we wanted to specify that the function returns something we could define it like the following:

fun person(function:()->Int) {}

While this is possible you probably wouldn’t want this because then your builder would have to return a value at the end:

person {
    return 35
}

Which doesn’t make much sense. And in my opinion makes it much more difficult to read.
However in our case we need to specify that it takes a specific type of function. A function that has access to all of the fields of the Person class. This is how the variables in person are assigned. We need to change our function argument to a function with receiver type.

fun person(function:Person.()->Unit ) {}

For break that down here are what the two parts are:

Receiver: Person.
Function: ()->Unit

Next we have to define a Person function that also takes a Function with Receiver Type.

Person {     
    constructor(init:Person.()->Unit): this() {               
        init()     
    }
}

The init variable holds the function, and we then execute that function call (which happens to set the fields in Person). While some of fields of Person get set, some don’t. In particular the name and contact fields need to be set. The same as before they are simply functions. But we have to add addition logic for the assignment. In the Person class to define name:

Person() {
    fun name(init:Name.() -> Unit) {     
        name = Name(init)
    }
}

As well we need to assign the contact as well:

Person(){
     fun name(init:Name.() -> Unit) {     
         name = Name(init)
     }

     fun contact(init:Contact.() -> Unit) {     
         contact = Contact(init)
     }
}

Operator Overloading

Defined addresses are easy, we have home and office. What if we wanted to add other addresses? We can add support for dynamic addresses.

We might as well point out that under the right conditions you can use Operator Overloading as well.
I say right conditions because I haven’t found many instances so far where I felt that operator overloading made my Type Safe Builder easier to read.

The HTML Type Safe Builder has a useful case for overloading the + operator.
Here’s an example for our address use case (even though I’m not a fan):

operator fun Addresses.plus(increment: Pair<String, String>) {
    addresses.plus(increment)
}

Which allows us to write:

addresses {
    +pairOf("mailing", "some address")
}

I find using Infix functions clearer here. Let’s take a look.

Infix Functions

Which would allow us to write code like the following:

addresses {             
    home = "668 Dewey Place, Fillmore, Mississippi, 6828"             
    work = "100 Wallaby Way"             
    "mailing" to "P.O. Box 275 ..."
    "emergency_address" to "2156"    
}

We can’t overload the assignment operator. But we can use infix functions. In this case we can create an infix function. Since it’s just a function it can be called like this:

addresses {
    home = "668 Dewey Place, Fillmore, Mississippi, 6828"
    work = "100 Wallaby Way"
    "mailing".to("P.O. Box 275 ...")
    "emergency_address".to("2156")
}

Our infix function would look like this:

class Addresses {
    val address = HashMap<String, String>()
    val home: String ...
    val work: String ...
    infix fun String.to(val value:String){       
        map.put(this, value)
    }
}

It’s worth pointing out that not only are we creating an infix function, but we are also creating an extension function at the same time. Even more interesting is that this extension function is within the context of the Addresses class. This means that the “to” function is only available in an Addresses object. This is how we are able to put additional custom addresses into the map.

Pitfalls of Type Safe Builders

I’ll now point out the problems I ran into while creating my first Type Safe Builder. The first problem that I ran into when creating a Type Safe Builder was with converting my existing classes.

Problems with vars

The variable properties weren’t a problem as I could update them to be lateinit and everything would compile.
For example if I had Person(var firstName: String, var lastName: String) I could do the following:

class Person() {
    lateinit var firstName: String
    lateinit var lastName: String

    constructor(first: String, last:String) {
        firstName = first
        lastName = last
    }
}

Problems with Vals

The read only vals were the problem. Since you can’t assign vals outside of the constructor I was stuck.
None of the examples I found online discussed this problem.

Builder Pattern to the Rescue

I ended up using the builder design pattern to get around this problem. I’d create a one to one mapping of the builder to the actual class. Each field in the class had a corresponding var in the builder class.

Lets look at an example. Say I had this class:

class Person(val firstName, val lastName)

Then I would create a builder:

class Person(val firstName, val lastName) {
    class Builder {
        lateinit var firstName
        lateinit var lastName
        fun build(): Person{
            return Person(firstName, lastName)
        }
    }

This created a way around not having the properties set in the constructor. Then we just need to add code for accepting a function, executing the function (thereby setting the properties):

class Person(val firstName, val lastName) {
    class Builder {
    lateinit var firstName
    lateinit var lastName
    fun build(init:Person.Builder.()->Unit): Person {
        init()
        return Person(firstName, lastName)
    }
}

Scoping functions appropriately

Placing the functions in the appropriate enclosing class is important and helps users of the Type Safe Builder use it properly.

For our Person example since the root most function person should be able to be created anywhere we likely create a function outside of the person class.

fun person(init: Person.Builder.()->Unit) : Person {
    return Person.Builder().build(init)
}

Note this can be shortened to this:

fun person(init: Person.Builder.()->Unit) = Person.Builder().build(init)

In contrast the name portion of the builder isn’t very useful outside of the context of the Person class. As a result we likely don’t have need for it on it’s own.

name {
}

Instead we defined the name function within the Person class. This effectively prevents the function from being called outside of the context of Person domain.

Missing Assignment

When I wrote my unit tests to verify that my Type Safe Builder was generating the class property correctly I discovered that it wasn’t. In particular nested functions in my Type Safe Builder weren’t working.

At least that is what I thought. They actually were working. I just wasn’t doing anything meaningful with their value.

We discussed this earlier, but as a recap it’s the difference between:

Person(){
    fun name(init:Name.() -> Unit) {
        Name(init)
    }
}

and

Person() {
    fun name(init:Name.() -> Unit){
        name = Name(init)
    }
}

Identifying good use cases for Type Safe Builders

As we’ve discussed before the builder design pattern is often needed. Additionally the more friendly a Type Safe Builder is the more code tends to go into it.

As a result the best use cases for Type Safe Builders are for objects where configuration is very important, and often there are multiple ways of configuring the object. And the configuration is deeply nested.

Also we probably want code generated for us that we can change to make the builder more user friendly. Having an annotation generate the builder isn’t likely as useful.

Android Studio supposedly has the ability to generate a builder for you. The option is never available for me (maybe for Java only ¯\_(ツ)_/¯). But I digress.

In short, when you find yourself needing to write code for configuring something over and over ask then a Type Safe Builder might be right for you. In my case this was happening when I had to create configuration objects for testing purposes.

Questions/Comments

Q: Why would you use this since Kotlin has named arguments?
A: If you have to generate complex nested objects do named arguments help you that much? I’d say that this comes down to your preferences. A better example than the one above (again the example was to make it easier to understand how to create a type safe builder) would be a layout example. Think of the difference between having to write out the code for generating a layout with a button and an edit text field.

val layout = LinearLayout(this)
layout.orientation = LinearLayout.VERTICAL
val name = EditText(this)
layout.addView(name)
val buttonClick = Button(this)
buttonClick.text = "Say Hello"
buttonClick.setOnClickListener {
    Toast.makeText(this, "Say Hello ${name.text}", Toast.LENGTH_LONG).show()
}
layout.addView(buttonClick)

Or being able to express it like Anko does:

verticalLayout {
    val name = editText()
    button("Say Hello") {
        onClick { toast("Hello, ${name.text}!") }
    }
}

What about non constructor configuration? The above example illustrates configuration that isn’t possible using constructors alone as there aren’t parameters for specifying orientation and additional child views.

These two should roughly be the same. I’d much rather express this as a Type Safe Builder.

Q: Why not create a constructor with default parameters and then create an instance over several lines, using named parameters?

A: Does it make sense to have default parameters for every argument? What happens if one of the parameters should have been specified but an incorrect default was used? Do you have to add additional checks to verify that the object was given non default arguments?

Leave a Reply

Your email address will not be published. Required fields are marked *