Type-Safe Builders
After we covered function literals with receivers, we can finally learn how to implement our own DSLs by combining them together with functions.
I know, sounds difficult (again), so let’s get to work.
Higher-Order Functions
When designing a DSL, we can use function literals with receivers just like we used simple lambdas.
Let’s take a look at the following code:
Nothing spectacular, just a simple class with mutable properties.
Now, let’s see how what we can do when we add a new function to the code:
In our example, the board function is a higher-order function. Moreover, it expects the function literal with a receiver as an argument.
What happens inside is pretty straightforward. We simply create a new Board instance, which we would like to return in the end. But before we do that, we must invoke the function passed as an argument. To put it simply- without that, the returned Board instance would have the default values. But when we invoke the board.init(), the title and colors will be changed accordingly to what we put in the main() function.
At this point, we started designing our first, custom DSL 🙂
Exercise 1
- Please implement a higher-order function named task, which takes a lambda expression as an argument (a function type with a receiver).
- The task function should create a new instance of the Task class.
class Task {
var title: String = ""
var description: String = ""
}
fun task(init: Task.() -> Unit): Task {
val task = Task()
task.init()
return task
}
fun main() {
val task = task {
title = "Task 1"
description = "Learn Kotlin"
}
}
Type-Safe Builders
Excellent! We already know how to implement a simple 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. And that’s where the DSL approach wins.
Let’s combine together Board and Task logic:
Firstly, we have to add a mutable list of tasks, where we keep all the tasks from our board.
Then, we simply put the task function inside the Board class (which adds the new task to the list). This way, we can use it when invoking the board function.
Just like we could set the title (title = “Important Tasks”), we can invoke any function on the board instance.
Let’s add a non-lambda function to the Board to see it even better:
And just like I said- we can invoke any Board instance member in our block 🙂
Exercise 2
- Please add a new class, Comment, which will have one property, also a comment.
- Nextly, please make the following code compile based on the knowledge we gathered in this lesson 🙂
enum class BoardColor {
BLACK, WHITE, GREEN, BLUE
}
class Board {
var title: String = ""
var color: BoardColor = BoardColor.BLUE
val tasks: MutableList = 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
}
class Task {
var title: String = ""
var description: String = ""
val comments: MutableList = mutableListOf()
fun comment(init: Comment.() -> Unit) {
val comment = Comment()
comment.init()
comments.add(comment)
}
}
class Comment {
var comment: String = ""
}
fun main() {
val board = board {
title = "Important Tasks"
color = BoardColor.GREEN
task {
title = "Task 1"
description = "Learn Kotlin"
comment {
comment = "Some comment 1"
}
}
task {
title = "Task 2"
description = "Master Kotlin"
}
}
}
Exercise 3
This time, please design a DSL for a restaurant menu. Each menu should have a name property, which will be initialized through a primary constructor. Moreover, it should consist of pages, which then can contain items.
class Menu(val name: String) {
private val pages: MutableList = mutableListOf()
fun page(init: MenuPage.() -> Unit) {
val page = MenuPage().apply(init)
pages.add(page)
}
}
class MenuPage {
private val items: MutableList
0 comments