Перейти к основному содержимому

DSL и функции с получателем в Kotlin

Разработчику Архитектору

DSL и функции с получателем

Напоминание: общие понятия функций в программе — функции в коде.

Когда вы пишете в Ktor:

routing {
get("/health") { call.respond(mapOf("status" to "ok")) }
}

блок routing { } выглядит как мини-язык конфигурации. На самом деле это обычный Kotlin: лямбда с получателем и extension-функции. Такой стиль называют DSL (предметно-ориентированный язык внутри языка).

Примеры в продакшене: Ktor, Gradle build.gradle.kts, HTML-генераторы, тестовые билдеры.

Теория синтаксиса: синтаксические конструкции. Стандартные apply / run: встроенные функции.


Словарь терминов

ТерминПростыми словами
DSLAPI, который читается как конфиг, а не как цепочка вызовов с точками.
ЛямбдаАнонимная функция: { x -> x * 2 }.
Receiver (получатель)Внутри блока доступен this как у метода класса.
Extension-функцияФункция «как будто метод» существующего типа: fun String.lastChar().
@DslMarkerЗащита от вызова «чужих» функций во вложенных DSL-блоках.

Обычная лямбда и лямбда с получателем

Обычная — аргумент передаётся явно:

val greet: (String) -> Unit = { name -> println("Hi, $name") }
greet("Ann")

С получателем — внутри блока this имеет тип T:

fun buildString(block: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.block() // вызов extension-лямбды на sb
return sb.toString()
}

val text = buildString {
append("Hello")
append(", ")
append("Kotlin")
}

Разбор:

  1. StringBuilder.() -> Unit — тип «функция без аргументов, но с receiver StringBuilder».
  2. Внутри { append("Hello") } компилятор подставляет this.append — как если бы вы писали методы у StringBuilder.
  3. Стандартная библиотека даёт те же приёмы: buildList, buildMap, apply, run, with.
val config = mutableMapOf<String, Any>().apply {
put("host", "localhost")
put("port", 8080)
}

apply возвращает сам объект после настройки.


Свой мини-DSL — меню

class MenuBuilder {
private val items = mutableListOf<String>()

fun item(text: String) {
items += text
}

fun build(): List<String> = items.toList()
}

fun menu(block: MenuBuilder.() -> Unit): List<String> =
MenuBuilder().apply(block).build()

val lunch = menu {
item("Суп")
item("Салат")
item("Чай")
}
ШагЧто видит новичок
menu { }Вызов функции с лямбдой
item("Суп")Как будто ключевое слово языка
РеальностьМетод MenuBuilder.item на неявном this

Ktor делает то же с Route: внутри routing { } receiver — объект маршрутизатора, get — его метод.


@DslMarker — не перепутать вложенные блоки

Без маркера во вложенном DSL можно случайно вызвать функцию «соседнего» уровня. Аннотация ограничивает видимость:

@DslMarker
annotation class HtmlTagMarker

@HtmlTagMarker
abstract class Tag {
protected val children = mutableListOf<Tag>()
}

@HtmlTagMarker
class Body : Tag() {
fun p(text: String) { /* добавить параграф */ }
}

@HtmlTagMarker
class Html : Tag() {
fun body(block: Body.() -> Unit) {
val b = Body()
b.block()
children += b
}
}

fun html(block: Html.() -> Unit): Html = Html().apply(block)

Вложенный body { p("…") } корректен; вызов p на уровне html { } компилятор отклонит.


Infix и операторы

infix fun Int.days(unit: String) = "$this $unit"

val period = 7 days "дней"

infix позволяет писать вызов без точки и скобок — для читаемости DSL.

Операторы для своих типов:

data class Vec2(val x: Int, val y: Int)

operator fun Vec2.plus(other: Vec2) = Vec2(x + other.x, y + other.y)

val sum = Vec2(1, 2) + Vec2(3, 4)

Используйте + только там, где сложение векторов ожидаемо — иначе DSL перестаёт быть понятным.


Упрощённый аналог Ktor routing

class RouteConfig {
val routes = mutableListOf<Pair<String, () -> Unit>>()

fun get(path: String, handler: () -> Unit) {
routes += path to handler
}
}

fun routing(block: RouteConfig.() -> Unit): RouteConfig =
RouteConfig().apply(block)

val app = routing {
get("/health") { println("ok") }
get("/version") { println("1.0") }
}

Реальный Ktor добавляет call, HTTP-методы, плагины — но скелет тот же.


Gradle Kotlin DSL

dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}

dependencies { } — функция с receiver типа DependencyHandlerScope. Вы «настраиваете» объект Gradle, а не вызываете десятки статических методов подряд.


Когда свой DSL избыточен

СитуацияЛучше
Три поля конфигурацииdata class + именованные аргументы
Одноразовый тестобычные функции
Команда слабо знает KotlinJSON/YAML конфиг

DSL окупается при иерархической настройке (маршруты, дерево UI, вложенные теги) и повторном использовании.


Связанные материалы


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).