Дорогие читатели,
Помогите сделать документацию лучше!

К сожалению текст все еще содержит некоторое количество ошибок, и мы просим вас найти и исправить их. Если бы каждый из посетителей исправил хотя бы одну ошибку, то мы смогли бы довести до совершенства весь материал всего за один день.


Типобезопасные строители

Идея строителей (builders) довольна популярна в сообществе Groovy. Строители позволяют объявлять данные в полудекларативном виде. Строители хороши для генерации XML, вёрстки компонентов UI, описания 3D сцен и многого другого...

В отличие от Groovy, Kotlin проверяет типы строителей, что делает их более приятными в использовании в большинстве юзкейсов.

Для прочих случаев Kotlin поддерживает Динамически типизированные строители (Dynamic types builders).

Пример типобезопасного строителя

Рассмотрим следующий код:

import com.example.html.* // смотрите объявления ниже

fun result(args: Array<String>) =
    html {
        head {
            title {+"XML кодирование с Kotlin"}
        }
        body {
            h1 {+"XML кодирование с Kotlin"}
            p  {+"этот формат может быть использован как альтернатва XML"}

            // элемент с атрибутом и текстовым содержанием
            a(href = "http://kotlinlang.ru") {+"Kotlin"}

            // смешанный контент
            p {
                +"Немного"
                b {+"смешанного"}
                +"текста. Посмотрите наш"
                a(href = "http://kotlinlang.org") {+"перевод"}
                +"документации Kotlin."
            }
            p {+"немного текста"}

            // контент генерируется в цикле
            p {
                for (arg in args)
                    +arg
            }
        }
    }

Всё это полностью корректный Kotlin-код. Здесь вы можете отредактировать и запустить пример с этим кодом прямо у себя в браузере.

Как это работает

Давайте рассмотрим механизм реализации типобезопасных строителей в Kotlin. Прежде всего, нам нужно определить модель, которую мы собираемся строить. В данном случае это HTML-тэги. Мы можем сделать это без труда с помощью нескольких классов. К примеру, HTML — это класс, который описывает тэг <html>, т.е. он определяет потомков, таких как <head> и <body>. (См. его объявление ниже.)

Теперь давайте вернёмся к вопросу почему мы можем писать вот такой код:

html {
 // ...
}

На самом деле, html является вызовом функции, которая принимает лямбда-выражение в качестве аргумента. Вот как эта функция определена:

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

Эта функция принимает один параметр-функцию под названием init. Тип этой функции: HTML.() -> Unitфункциональный тип с объектом-приёмником. Это значит, что нам нужно передать экземпляр класса HTML (приёмник) в функцию, и мы сможем обращаться к членам объекта в теле этой функции. Обращение происходит через ключевое слово this:

html {
    this.head { /* ... */ }
    this.body { /* ... */ }
}

(head и body — члены класса HTML)

Теперь this может быть опущено, и мы получим что-то, что уже очень похоже на строителя:

html {
    head { /* ... */ }
    body { /* ... */ }
}

Итак, что же делает этот вызов? Давайте посмотрим на тело функции html, объявленной выше. Она создаёт новый экземпляр HTML, затем инициализирует его путём вызова функции, которая была передана в аргументе (в нашем примере это сводится к вызову head и body у объекта HTML), и после этого возвращает его значение. Это в точности то, что и должен делать строитель.

Функции head и body в классе HTML объявлены схоже с функцией html. Единственное отличие в том, что они добавляют отстроенные экземпляры в коллекцию children заключающего экземпляра HTML:

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
}

На самом деле эти две функции делают одно и тоже, поэтому мы можем использовать обобщённую версию, initTag:

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

Теперь наши функции выглядят очень просто:

fun head(init: Head.() -> Unit) = initTag(Head(), init)

fun body(init: Body.() -> Unit) = initTag(Body(), init)

И мы можем использовать их для постройки тэгов <html> и <body>.

Ещё одна вещь, которую следует обсудить, это добавление текста в тело тэга. В примере выше мы используем такой синтаксис:

html {
    head {
        title {+"XML кодирование с Kotlin"}
    }
    // ...
}

Итак, мы просто добавляем строку в тело тэга, приписав + перед текстом, что ведёт к вызову префиксной операции unaryPlus(). Эта операция определена с помощью функции-расширения unaryPlus(), которая является членом абстрактного класса TagWithText (родителя Title).

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

Иными словами, префикс + оборачивает строку в экземпляр TextElement и добавляет его в коллекцию children.

Всё это определено в пакете com.example.html, который импортирован в начале примера выше. В последней секции вы можете прочитать полное описание определений в этом пакете.

Контроль области видимости: @DslMarker (с версии 1.1)

При использовании DSL может возникнуть проблема, когда слишком много функций могут быть вызваны в определённом контексте. Мы можем вызывать методы каждого неявного приёмника внутри лямбды, и из-за этого может возникать противоречивый результат, как, например, тэг head внутри другого тэга head:

html {
    head {
        head {} // должен быть запрещён
    }
    // ...
}

В этом примере должны быть доступны только члены ближайшего неявного приёмника this@head; head() является членом другого приёмника — this@html, поэтому его вызов в другом контексте должен быть запрещён.

Для решения этой проблемы в Kotlin 1.1 был введен специальный механизм для управления областью приёмника.

Чтобы заставить компилятор запускать контрольные области, нам нужно только аннотировать типы всех получателей, используемых в DSL, той же маркерной аннотацией. Например, для HTML Builders мы объявляем аннотацию @HTMLTagMarker:

@DslMarker
annotation class HtmlTagMarker

Аннотированный класс называется DSL-маркером, если он помечен аннотацией @DslMarker.

В нашем DSL все классы тэгов расширяют один и тот же суперкласс Tag. Нам достаточно аннотировать @HtmlTagMarker только суперкласс, и после этого компилятор Kotlin обработает все унаследованные классы в соответствии с аннотацией:

@HtmlTagMarker
abstract class Tag(val name: String) { ... }

Нам не нужно помечать классы HTML или Head аннотацией @HtmlTagMarker, потому что их суперкласс уже аннотирован:

class HTML() : Tag("html") { ... }
class Head() : Tag("head") { ... }

После добавления этой аннотации, компилятор Kotlin знает, какие неявные приёмники являются частью того же DSL, и разрешает обращаться только к членам ближайших приёмников:

html {
    head {
        head { } // ошибка: член внешнего приёмника
    }
    // ...
}

Обратите внимание, что всё ещё возможно вызывать члены внешнего приёмника, но для этого вам нужно указать этот приёмник явно:

html {
    head {
        this@html.head { } // всё работает
    }
    // ...
}

Полное описание пакета com.example.html

Перед вами содержание пакета com.example.html (представлены только элементы, использованные в примере выше). Он строит HTML дерево и активно использует расширения и лямбды с приёмниками.

Примечание: Аннотация @DslMarker доступна в 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
}