Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Type-Safe Builders in Kotlin Using Function Literals with Receivers

Tech 1

Type-safe builders in Kotlin enable the creation of domain-specific languages (DSLs) for constructing hierarchical data structures in a semi-declarative manner. This approach leverages well-named functions as builders combined with function literals that have receivers, ensuring static type safety.

Common applications include generating markup languages like HTML or XML, programmatically arranging UI components in frameworks such as Anko, and configuring web server routes in Ktor.

Example of a Type-Safe Builder

Consider the following Kotlin code that generates HTML:

import com.example.html.*

fun generateHtml() = html {
    head {
        title { +"Kotlin XML Encoding Example" }
    }
    body {
        h1 { +"Kotlin XML Encoding Example" }
        p { +"This format serves as an alternative to XML markup." }
        a(href = "https://kotlinlang.org") { +"Kotlin" }
        p {
            +"This text contains"
            b { +"mixed" }
            +"content. Learn more at the"
            a(href = "https://kotlinlang.org") { +"Kotlin" }
            +"project."
        }
        p { +"Additional text here." }
        p {
            for (item in arguments) +item
        }
    }
}

This code is valid Kotlin and can be executed online with modifications.

Underlying Mechanism

To implement a type-safe builder, start by defining a model for the structure being built. For HTML, create classses representing tags like HTML, Head, and Body.

The expression html { ... } calls a function html that takes a lambda with a receiver of type HTML. The function is defined as:

fun html(initializer: HTML.() -> Unit): HTML {
    val htmlInstance = HTML()
    htmlInstance.initializer()
    return htmlInstance
}

The parameter initializer has type HTML.() -> Unit, a function type with a receiver. This allows accessing members of the HTML instance with in the lambda using this, which can be omitted for brevity. Inside the lambda, calls like head { ... } and body { ... } invoke member functions of HTML.

The html function creates an HTML object, applies the initializer to set up its children, and returns it, functioning as a builder.

Functions such as head and body in the HTML class add constructed instances to a children collection. They can be generalized with a helper function:

protected fun <T : Element> createTag(tag: T, setup: T.() -> Unit): T {
    tag.setup()
    children.add(tag)
    return tag
}

Thus, head and body are implemented as:

fun head(setup: Head.() -> Unit) = createTag(Head(), setup)
fun body(setup: Body.() -> Unit) = createTag(Body(), setup)

To insert text into tags, use the unary plus operator (+). For example, +"text" calls an extension function on String:

operator fun String.unaryPlus() {
    children.add(TextElement(this))
}

This wraps the string in a TextElement and adds it to the children list.

Scope Control with @DslMarker

In DSLs, limiting function availability to the appropriate context prevents errors, such as nesting head inside another head. Kotlin 1.1 introduced the @DslMarker annotation to control receiver scope.

Define a DSL marker annotation:

@DslMarker
annotation class HtmlDslMarker

Annnotate a common superclass for all tags:

@HtmlDslMarker
abstract class Tag(val tagName: String) { ... }

Classes like HTML and Head inherit this annotation. The compiler then restricts implicit receiver access to the nearest layer, disallowing calls like head { head { } } within a head block. To call an outer receiver's member, explicitly specify it:

html {
    head {
        this@html.head { } // Allowed
    }
}

Complete Package Definition

The com.example.html package below implements the HTML builder with extensions and lambda receivers. Note that @DslMarker requires Kotlin 1.1 or later.

package com.example.html

interface Element {
    fun render(output: StringBuilder, indentation: String)
}

class TextElement(private val content: String) : Element {
    override fun render(output: StringBuilder, indentation: String) {
        output.append("$indentation$content\n")
    }
}

@DslMarker
annotation class HtmlDslMarker

@HtmlDslMarker
abstract class Tag(private val tagName: String) : Element {
    private val childElements = mutableListOf<Element>()
    private val attributeMap = mutableMapOf<String, String>()

    protected fun <T : Element> initializeTag(tag: T, config: T.() -> Unit): T {
        tag.config()
        childElements.add(tag)
        return tag
    }

    override fun render(output: StringBuilder, indentation: String) {
        output.append("$indentation<$tagName${formatAttributes()}>\n")
        for (child in childElements) {
            child.render(output, indentation + "  ")
        }
        output.append("$indentation</$tagName>\n")
    }

    private fun formatAttributes(): String {
        val result = StringBuilder()
        for ((key, value) in attributeMap) {
            result.append(" $key=\"$value\"")
        }
        return result.toString()
    }

    override fun toString(): String {
        val builder = StringBuilder()
        render(builder, "")
        return builder.toString()
    }
}

abstract class TextTag(name: String) : Tag(name) {
    operator fun String.unaryPlus() {
        childElements.add(TextElement(this))
    }
}

class HTML : TextTag("html") {
    fun head(config: Head.() -> Unit) = initializeTag(Head(), config)
    fun body(config: Body.() -> Unit) = initializeTag(Body(), config)
}

class Head : TextTag("head") {
    fun title(config: Title.() -> Unit) = initializeTag(Title(), config)
}

class Title : TextTag("title")

abstract class BodyElement(name: String) : TextTag(name) {
    fun bold(config: Bold.() -> Unit) = initializeTag(Bold(), config)
    fun paragraph(config: Paragraph.() -> Unit) = initializeTag(Paragraph(), config)
    fun heading(config: Heading.() -> Unit) = initializeTag(Heading(), config)
    fun link(target: String, config: Link.() -> Unit) {
        val link = initializeTag(Link(), config)
        link.url = target
    }
}

class Body : BodyElement("body")
class Bold : BodyElement("b")
class Paragraph : BodyElement("p")
class Heading : BodyElement("h1")

class Link : BodyElement("a") {
    var url: String
        get() = attributeMap["href"]!!
        set(value) {
            attributeMap["href"] = value
        }
}

fun html(config: HTML.() -> Unit): HTML {
    val htmlObj = HTML()
    htmlObj.config()
    return htmlObj
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.