Type-safe builders

With the two previous sections (infix functions and operator overloading), we have a good foundation for building fantastic DSLs. A DSL is a language that is specialized to a particular domain, in contrast to general-purpose language (GPL). Classic examples of DSLs (even when people don't realize it) are HTML (markup) and SQL (relational database queries).  

Kotlin provides many features to create internal DSLs (a DSL that runs internally inside a host GPL), but there is one feature that we still need to cover, type-safe builders. Type-safe builders let us define data in a (semi) declarative way and are very useful to define GUIs,  HTML markup, XML, and others.

An example of a beautiful Kotlin DSL is TornadoFX. TornadoFX (https://tornadofx.io/) is DSL for creating JavaFX applications.

We write an FxApp class that extends tornadofx.App and receives a tornadofx.View class (a class reference, not an instance):

import javafx.application.Application
import tornadofx.*

fun main(args: Array<String>) {
Application.launch(FxApp::class.java, *args)
}

class FxApp: App(FxView::class)

class FxView: View() {
override val root = vbox {
label("Functional Kotlin")
button("Press me")
}
}

In less than 20 lines of code, including imports and main function, we can create a GUI application:

Of course, right now, it doesn't do anything, but it is simple to create a JavaFX Application with TornadoFX, if you compare it with Java. People with JavaFX experience could say that you can achieve something similar with FXML (a declarative XML language designed to build JavaFX layouts), but as with any other XML file, writing and maintaining is hard, and TornadoFX's DSL is simpler, flexible, and is compiled with Kotlin's type-safety.

But how do type-safe builders work?

Let's start with an example from the Kotlin Standard Library:

val joinWithPipe = with(listOf("One", "Two", "Three")){
joinToString(separator = "|")
}

We can find with blocks in other languages, such as JavaScript and Visual Basic (including .Net). A with block is a language construct that lets us use any property or method on the value that we pass as a parameter. But in Kotlin, with is not a reserved keyword but rather a normal function with a special type of parameter.

Let's have a look at the with declaration:

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}

The first parameter is any value of type T, a receiver (as in extension function?) and the second one, block, is a function of type T.() -> R. In Kotlin's documentation, this kind of function is named function type with receiver and with any instance of T, we can call the block function. No worries about the inline modifier, we'll cover it in the next section.

A trick to understanding the function type with receiver is to think of it as an extension function. Have a look at the declaration with that familiar dot (.), and inside the function, we can use any member of the receiver type using this, as in extension functions.

What about another example? Let's have a look at it:

val html = buildString {
append("<html> ")
append(" <body> ")
append(" <ul> ")
listOf(1, 2, 3).forEach { i ->
append(" <li>$i</li> ")
}
append(" <ul> ")
append(" </body> ")
append("</htm

l>"
)
}

The buildString function receives a StringBuilder.() -> Unit parameter and returns a String; the declaration is astonishingly simple:

public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
StringBuilder().apply(builderAction).toString()

The apply function is an extension function similar to with but instead of returning R, returns the receiver instance. Usually, apply is used for initializing and instance:

public inline fun <T> T.apply(block: T.() -> Unit): T {    
block()
return this
}

As you can see, all these functions are very simple to understand, but they increase Kotlin's usefulness and readability a great deal.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.47.166