Skip to content

Latest commit

 

History

History
192 lines (159 loc) · 10.4 KB

chapter10_2.adoc

File metadata and controls

192 lines (159 loc) · 10.4 KB

10.2. Дополнительные главы

Kotlin DSL

В этом разделе мы поговорим о так называемых DSL на базе Kotlin. При изучении современных методов программирования вы будете часто наталкиваться на эту аббревиатуру. Это может выглядеть страшно, но DSL означает всего лишь "Domain Specific Language" или, по-русски, предметно-ориентированный язык. По смыслу это значит, что мы на базе Kotlin создаём какой-то более узкий по объёму возможностей язык для описания какой-то предметной области.

Классическим примером DSL на базе Kotlin является библиотека kotlinx.html. Она позволяет с помощью языка Kotlin генерировать содержимое HTML-файлов.

Отступление про HTML-файлы

HTML = HyperText Markup Language = Язык разметки гипертекста. Это описательный язык (не путать с языком программирования!), по сути это всего лишь способ задания текстовых файлов с расширенным набором возможностей. HTML позволяет разбивать "сырой" текст на параграфы, описывать внутри него списки, таблицы, превращать часть текста в гиперссылки (на странички Интернета), задавать шрифт и цвет отображаемого текста, создавать специальные формы для редактирования информации читателем и так далее.

Основное использование HTML — Интернет. Когда браузер (Internet Explorer, Mozilla Firefox, Google Chrome, …​) заходит на Интернет-страничку, он скачивает с сервера HTML-файл. HTML содержит внутри себя "размеченный текст", а любой из перечисленных браузеров умеет показывать HTML-файлы пользователю в соответствии с правилами разметки. Эти правила базируются на так называемых "тегах". Например, тег <p> обозначает начало параграфа, а тег </p> конец параграфа, при этом между этими тегами находится сам параграф. Короткий HTML-файл ниже приведёт к изображению в браузере текста, разбитого на два параграфа.

<html><body>
<p>First chapter</p>
<p>Second chapter</p>
</body></html>

Базовые возможности формата HTML как раз и сводятся к использованию подобных пар <name> / </name>. Например, в скобки <html>…​</html> берётся всё содержимое файла, <b>…​</b> обозначает текст, отображаемый жирным шрифтом (bold), <table>…​</table> таблицу, <ul>…​</ul> непронумерованный список \ и так далее. Теги могут вкладываться друг в друга по определённым правилам. Скажем, ряд таблицы <tr>…​</tr> существует только внутри таблицы, а ячейка <td>…​</td> только внутри ряда.

Посмотрите на пример функции, генерирующей HTML-файл с описанием двумерной таблицы, без помощи DSL.

fun List<List<String>>.convertToHtmlTable(): String {
    val sb = StringBuilder()
    sb.append("<html>")
    sb.append("<body>")
    sb.append("<table>")
    for (row in this) {
        sb.append("<tr>")
        for (data in row) {
            sb.append("<td>")
            sb.append(data)
            sb.append("</td>")
        }
        sb.append("</tr>")
    }
    sb.append("</table>")
    sb.append("</body>")
    sb.append("</html>")
    return sb.toString()
}

Функция последовательно открывает теги <name> и закрывает их </name>, вкладывая их друг в друга подобно стеку. Минус подобного описания "в лоб" состоит в том, что мы сами должны контролировать своевременное закрытие тегов, а также соблюдение правил их вложения друг в друга.

Структура DSL на примере kotlinx.html

Библиотека kotlinx.html для генерации HTML-файлов использует код следующего вида

fun List<List<String>>.convertToHtmlTableUsingKotlinxHtml(): String {
    val inputList = this
    val sb = StringBuilder()
    sb.appendHTML().html {
        body {
            table {
                for (row in inputList) {
                    tr {
                        for (data in row) {
                            td { +data }
                        }
                    }
                }
            }
        }
    }
    return sb.toString()
}

Эта функция преобразует двумерный список в таблицу, при этом один ряд списка становится одним рядом HTML-таблицы, а один элемент списка, соответственно, элементом HTML-таблицы. Код построен так, что каждому HTML-тегу, например, паре <table> / `</table>, соответствует код вида

table {
    // Что-то между тегами
}

По факту table { …​ } является вызовом функции высшего порядка, а { …​ } лямбдой, передаваемой в эту функцию.

DSL позволяет возложить контроль за правилами вложения тегов друг в друга и за их закрытием на компилятор Kotlin. Например, DSL не позволит вам создать ряд таблицы на верхнем уровне, без создания самой таблицы. Разумеется, работает DSL без помощи колдовства и магии. В основе описания функций высшего порядка вроде table { …​ } лежит механизм type-safe builders (приблизительный перевод — типо-безопасные построители).

Пример type-safe builder

Рассмотрим в качестве примера, как работают теги верхнего уровня.

private class HTML(val sb: StringBuilder) {
    fun myBody(init: HTMLBody.() -> Unit): HTMLBody {
        val body = HTMLBody(sb)
        sb.append("<body>")
        body.init()
        sb.append("</body>")
        return body
    }
}

Это класс, экземпляр которого соответствует содержимому тега <html></html>. По правилам разметки HTML, внутри данного тега может встретиться тег <body>. Этому тегу будет соответствовать вызов функции

myBody {
    // ...
}

При этом содержимому тега <body>…​</body> соответствует параметр init: HTMLBody.() → Unit, а класс HTMLBody определяется подобно классу HTML. Любому тегу, который можно создать внутри <body>…​</body>, например тегу параграфа <p>, должна соответствовать функция класса HTMLBody, описанная подобно myBody.

Таким образом, класс HTML создаёт экземпляр класса HTMLBody, тот, в свою очередь, экземпляр класса параграф и так далее. Кто же в DSL отвечает за создание объекта верхнего уровня HTML?

private fun StringBuilder.myHtml(init: HTML.() -> Unit): HTML {
    val html = HTML(this)
    append("<html>")
    html.init()
    append("</html>")
    return html
}

А использование этого игрушечного DSL выглядит следующим образом. В начале, как видите, был StringBuilder. А уже из него были сформированы HTML и HTMLBody. Обратите также внимание на оператор +s и соответствующее ему объявление operator fun unaryPlus(). Он предназначен для добавления в определённое место HTML простого текста.

fun generateSimpleHtml(s: String): String {
    val sb = StringBuilder()
    sb.myHtml {
        myBody {
            +s
        }
    }
    return sb.toString()
}

Упражнения

Вы можете посмотреть примеры в srс/lesson10/task2/Html.kt и решить задачу про генерацию нумерованного списка. Задача довольно лёгкая и её решение не должно вызвать у вас трудностей. Вы можете также попытаться использовать kotlinx.html для решения задач про HTML-файлы в уроке 7.

В качестве свободного чтения вы можете прочитать раздел официальной справки про построение DSL.