More language structures of kotlin - > type safe builder

Posted by austinderrick2 on Fri, 28 Jan 2022 19:04:13 +0100

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)

When using DSL, you may encounter the problem that too many functions can be called in the context. We can call the method of each available implicit receiver inside the lambda expression, so we get an inconsistent result, just like the head tag inside another head
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
}