Kotlin Type-Safe Builders. Make Your Custom DSL



Kotlin Type-Safe Builders. Make Your Custom DSL | ninjasquad

1. Introduction

Hi! 🙂 In this publication, we will learn how to implement statically-typed, type-safe Kotlin builders which we can use to implement our own DSLs.

Firstly, we will discuss what exactly DSLs are, their benefits, and why Kotlin is a good choice for creating them. Then, we will learn more about function literals with a receiver. And finally, we will put together all this knowledge and implement our own DSL.

Note: This article has been created based on my course Kotlin Handbook. Learn Through Practice. If you are looking for high-quality, step-by-step Kotlin learning path, then you should definitely check it out.

2. What is a DSL?

A DSL is an acronym for Domain-Specific Language.

It’s nothing else than a specialized language designed to solve problems or tasks within a specific domain.

Unlike general-purpose languages (like Kotlin itself), which are designed for a wide range of applications, DSLs focus on easy-to-understand syntax tailored to a particular problem domain.

Great examples can be:

  • HTML for creating web pages,
  • Gradle for building automation,
  • or SQL for querying databases.

3. The Purpose and Benefits of DSLs

The main purpose of DSLs is to help programmers to deal with complex hierarchical data structures in an easy way. 

So, a well-designed domain-specific language provides (among others) the following benefits: 

  • expressiveness– helps us to express complex ideas and logic using a more concise and natural syntax,
  • readability– the code we produce is simply easier to read and understand, 
  • maintainability– a modular, and organized code structure makes it easier to maintain and extend our programs. 

4. Is Kotlin a Good Choice For DSLs?

Short answer- yes

Kotlin is designed to be a concise, readable, and expressive programming language. When we combine together properly named functions and function literals with receiver, we can implement truly type-safe Kotlin builders.

Additionally, lambda expressions, scoping literals, extension, or infix functions help us to organize everything in an even cleaner way.

If you are looking for real-life usage examples, then we can find type-safe builders for example, when configuring routes in Ktor, or in Gradle’s Kotlin DSL.

5. Function Literals with Receiver

With all of that being said, let’s start the practice part by explaining the concept of function literals with receiver.

In simple words, it is nothing else than a combination of lambda expression:

val hello: (String) -> String = { name -> "Hello, $name" }

fun main() {
  val greeting = hello("Pjoter")
  
  println(greeting)  // Hello, Pjoter
}

And extension function:

fun String.hello() : String = "Hello, $this"

fun main() {
  val greeting = "Pjoter".hello()
  
  println(greeting)  // Hello, Pjoter
}

Which combined together form:

val hello: String.() -> String = { "Hello, $this" }

fun main() {
  val greeting = "Pjoter".hello()
  
  println(greeting)  // Hello, Pjoter
}

In our example, the String object, for which we invoke the function is a receiver and it implicitly becomes this inside the function. This way, we can access all its members, like public fields, inside our function (even without this keyword).

Great, but how does this relate to creating DSLs in Kotlin?

Well, we can pass function literals with receiver as arguments to high-order functions (functions that take other functions as parameters).

I know, a mumbo jumbo, but everything will become clear by the end of this tutorial 😉

6. Implement Custom Kotlin DSL

6.1 Add High-Order Function

As the first step, let’s take a look at the example class:

enum class BoardColor {
  BLACK, WHITE, GREEN, BLUE
}

class Board {
  var title: String = ""
  var color: BoardColor = BoardColor.BLUE
}

fun main() {
  val board = Board()
  board.title = "Important Tasks"
  board.color = BoardColor.GREEN
}

Nothing spectacular, right? Just a simple class with mutable properties.

So as the next step, let’s add a higher-order function, which expects the function literal with a receiver as an argument:

enum class BoardColor {
  BLACK, WHITE, GREEN, BLUE
}

class Board {
  var title: String = ""
  var color: BoardColor = BoardColor.BLUE
}

fun board(init: Board.() -> Unit): Board {
  val board = Board()
  board.init()
  return board
}

fun main() {
  val board = board {
    title = "Important Tasks"
    color = BoardColor.GREEN
  }
}

The board function is pretty straightforward- firstly, we create a new Board instance, and then we invoke the init function on it. After that, we return the updated object instance.

When we look at the main function, we can see that the only thing we do is the board function invocation.

But there is no parenthesis- “()”- right?

Well, technically we could use them: board ( {/*some code*/ } ). But in Kotlin, when the last parameter of a function is a function, then a lambda expression passed as the corresponding argument can be placed outside the parentheses (docs). In our case- the function has only one parameter, so we don’t need parenthesis, at all.

6.2 Kotlin Type-Safe Builder

And although at this point our logic looks like overkill, life isn’t always that easy and we may need to add more hierarchy to our code.

Let’s consider the following code snippet:

enum class BoardColor {
  BLACK, WHITE, GREEN, BLUE
}

class Task {
  var title: String = ""
  var description: String = ""
}

class Board {
  var title: String = ""
  var color: BoardColor = BoardColor.BLUE
  var tasks: MutableList<Task> = mutableListOf()
}

fun main() {
  val taskOne = Task() 
  taskOne.title = "Task 1"
  taskOne.description = "Task 1 description"  
  
  val taskTwo = Task() 
  taskTwo.title = "Task 2"
  taskTwo.description = "Task 2 description"   
  
  val tasks = mutableListOf(taskOne, taskTwo)  
    
  val board = Board() 
  board.title = "Important Tasks"
  board.color = BoardColor.GREEN
  board.tasks = tasks	  
}

In this case, every Board object can contain multiple Tasks instances.

When we look at the main function, it’s pretty hard to see the hierarchy of objects at first glance.

And now, let’s analyze the Kotlin type-safe builder approach:

enum class BoardColor {
  BLACK, WHITE, GREEN, BLUE
}

class Task {
  var title: String = ""
  var description: String = ""
}

class Board {
  var title: String = ""
  var color: BoardColor = BoardColor.BLUE
  var tasks: MutableList<Task> = mutableListOf()
  
  fun task(init: Task.() -> Unit) {
    val task = Task()
    task.init()
    tasks.add(task)
  }
}

fun board(init: Board.() -> Unit): Board {
  val board = Board()
  board.init()
  return board
}

fun main() {
   val board = board {
     title = "Important Tasks"
     color = BoardColor.GREEN
    
     task {
       title = "Task 1"
       description = "Task 1 description"  
     }
    
     task {
       title = "Task 2"
       description = "Task 2 description" 
     }
  }  
}

We brought back the board function and implemented a new task method inside the Board.

When we implement our lambda inside the main function, we can access the task function, just like we access the title, or color property of the Board object.

To better visualize, let’s use the explicit this and parenthesis:

val board = board {
  this.title = "Important Tasks"
  this.color = BoardColor.GREEN
    
  this.task( {
    this.title = "Task 1"
    this.description = "Task 1 description"  
  } )
    
  this.task( {
    this.title = "Task 2"
    this.description = "Task 2 description" 
  } )
}  

To rephrase- the object, for which we invoke the function is a receiver and it implicitly becomes this inside the function.

So, our board function first creates a new object and then invokes passed lambda on it- which sets the title, color and invokes the task method twice.

On the other hand, the task function also creates a new, “plain” object and also invokes passed lambda on it- this time, setting the title and description values for it.

And basically, we’ve just created our first, simple Kotlin type-safe builder 🙂

7. Summary

In this article, we’ve learned not only how to create our custom Kotlin DSL, but also the key features which make this possible.

Of course, this is just an introduction, and if you are interested in learning more, for example about scope control, then let me know in the comments section 🙂





Source: Internet

Leave a Comment

We are offering free coding tuts

X