By using properly named functions as builders, combined with function literals with recipients, you can create type safe, statically typed builders in Kotlin
A type safe builder can create a domain specific language (DSL) based on Kotlin, which is suitable for building complex hierarchical data structures in a semi declarative way. Here are some sample application scenarios for the Builder:
- use Kotlin code to generate markup language, such as HTML or XML;
- layout UI components programmatically: Anko;
- configure routing for Web server: Ktor.
A type safe builder example
Consider the following code
import com.example.html.* // See the following statement fun result() = html { head { title { +"XML encoding with Kotlin" } } body { h1 { +"XML encoding with Kotlin" } p { +"this format can be used as an alternative markup to XML" } // An element with attributes and text content a(href = "http://kotlinlang.org") { +"Kotlin" } // Mixed content p { +"This is some" b { +"mixed" } +"text. For more see the" a(href = "http://kotlinlang.org") { +"Kotlin" } +"project" } p { +"some text" } // The content generated by the following code p { for (arg in args) +arg } } }
This is completely legal Kotlin code. You can run the above code online here (modify it and run it in the browser)
Implementation principle
Let's take a look at the mechanism for implementing the type safety builder in Kotlin. First, we need to define the model we want to build. In this case, we need to model HTML tags. It can be easily done with some classes. For example, html is a class that describes < HTML > tags, that is, it defines sub tags such as < head > and < body >. (see its statement below.)
Now, let's recall why we can write this in code
html { // ...... }
html is actually a function call that takes a lambda expression as an argument. This function is defined as follows
fun html(init: HTML.() -> Unit): HTML { val html = HTML() html.init() return html }
This function accepts a parameter called init, which is itself a function. The type of this function is HTML () - > unit, which is a function type with receiver. This means that we need to pass an HTML type instance (receiver) to the function, and we can call the members of the instance inside the function. The recipient can be accessed through this keyword
html { this.head { ...... } this.body { ...... } }
(head and body are member functions of HTML.)
Now, as usual, this can be omitted, and what we get looks very much like a builder
html { head { ...... } body { ...... } }
So, what does this call do? Let's look at the body of the HTML function defined above. It creates a new instance of HTML, initializes it by calling the function passed in as a parameter (in our example, it boils down to calling head and body on the HTML instance), and then returns this instance. This is exactly what the builder should do.
The definition of head and body functions in HTML class is similar to HTML. The only difference is that they add the built instance to the children collection containing the HTML instance
fun head(init: Head.() -> Unit) : Head { val head = Head() head.init() children.add(head) return head } fun body(init: Body.() -> Unit) : Body { val body = Body() body.init() children.add(body) return body }
In fact, these two functions do the same thing, so we can have a generic version, initTag
protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T { tag.init() children.add(tag) return tag }
So now our function is very simple
fun head(init: Head.() -> Unit) = initTag(Head(), init) fun body(init: Body.() -> Unit) = initTag(Body(), init)
And we can use them to build < head > and < body > tags.
Another thing to discuss here is how to add text to the tag body. In the above example, we wrote
html { head { title {+"XML encoding with Kotlin"} } // ...... }
So basically, we just put a string into a label body, but there is a small + in front of it, so it is a function call, calling a prefix unaryPlus() operation. This operation is actually defined by an extension function unaryPlus(), which is a member of the TagWithText abstract class (the parent class of Title)
operator fun String.unaryPlus() { children.add(TextElement(this)) }
So what prefix + does here is wrap a string into a TextElement instance and add it to the children collection to make it an appropriate part of the label tree.
All of these packages are imported at the top of the builder example above example. Defined in HTML. In the last section, you can read the complete definition of this package
Scope control: @ dslmark (r since 1.1)
html { head { head {} // Should be prohibited } // ...... }
In this example, there must be only the implicit recipients of the nearest layer this@head Members available; head() is the external receiver this@html So it must be illegal to call it.
In order to solve this problem, a special mechanism for controlling the scope of the receiver is introduced in Kotlin 1.1.
In order for the compiler to start controlling tags, we just have to mark the types of all recipients used in DSL with the same tag annotation. For example, for the HTML builder, we declare an annotation @ HTMLTagMarker
@DslMarker annotation class HtmlTagMarker
If an annotation class is annotated with @ DslMarker annotation, the annotation class is called DSL tag.
In our DSL, all Tag classes extend the same superclass Tag. Just use @ HtmlTagMarker to label the superclass is enough, and then the Kotlin compiler will treat all inherited classes as labeled
@HtmlTagMarker abstract class Tag(val name: String) { ...... }
We don't have to tag HTML or Head classes with @ HtmlTagMarker because their superclasses have been tagged
class HTML() : Tag("html") { ...... } class Head() : Tag("head") { ...... }
After adding this annotation, the Kotlin compiler knows which implicit recipients are part of the same DSL, and only members of the recipients of the nearest layer are allowed to be called
html { head { head { } // Error: member of external recipient } // ...... }
Note that members of an external recipient can still be called, but to do this, you must specify the recipient explicitly
html { head { this@html.head { } // probably } // ...... }
com. example. Complete definition of HTML package
This is com example. Definition of HTML package (only the elements used in the above example). It builds an HTML tree. The code makes extensive use of extension functions and lambda expressions with receivers.
Please note that the @ DslMarker annotation is only available from Kotlin 1.1
package com.example.html interface Element { fun render(builder: StringBuilder, indent: String) } class TextElement(val text: String) : Element { override fun render(builder: StringBuilder, indent: String) { builder.append("$indent$text\n") } } @DslMarker annotation class HtmlTagMarker @HtmlTagMarker abstract class Tag(val name: String) : Element { val children = arrayListOf<Element>() val attributes = hashMapOf<String, String>() protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T { tag.init() children.add(tag) return tag } override fun render(builder: StringBuilder, indent: String) { builder.append("$indent<$name${renderAttributes()}>\n") for (c in children) { c.render(builder, indent + " ") } builder.append("$indent</$name>\n") } private fun renderAttributes(): String { val builder = StringBuilder() for ((attr, value) in attributes) { builder.append(" $attr=\"$value\"") } return builder.toString() } override fun toString(): String { val builder = StringBuilder() render (builder, "") return builder.toString() } } abstract class TagWithText(name: String) : Tag(name) { operator fun String.unaryPlus() { children.add(TextElement(this)) } } class HTML : TagWithText("html") { fun head(init: Head.() -> Unit) = initTag(Head(), init) fun body(init: Body.() -> Unit) = initTag(Body(), init) } class Head : TagWithText("head") { fun title(init: Title.() -> Unit) = initTag(Title(), init) } class Title : TagWithText("title") abstract class BodyTag(name: String) : TagWithText(name) { fun b(init: B.() -> Unit) = initTag(B(), init) fun p(init: P.() -> Unit) = initTag(P(), init) fun h1(init: H1.() -> Unit) = initTag(H1(), init) fun a(href: String, init: A.() -> Unit) { val a = initTag(A(), init) a.href = href } } class Body : BodyTag("body") class B : BodyTag("b") class P : BodyTag("p") class H1 : BodyTag("h1") class A : BodyTag("a") { var href: String get() = attributes["href"]!! set(value) { attributes["href"] = value } } fun html(init: HTML.() -> Unit): HTML { val html = HTML() html.init() return html }