Kotlin dsl что это
Перейти к содержимому

Kotlin dsl что это

  • автор:

Котлин Д.С.Л .: от теории к практике

Я попытаюсь объяснить синтаксис языка как можно более простым, однако статья по-прежнему привлекательна для разработчиков, которые рассматривают Kotlin как язык для пользовательского построения DSL. В конце статьи я упомяну о недостатках Kotlin, которые стоит учитывать. Представленные фрагменты кода относятся к Kotlin версии 1.2.0 и доступны на GitHub.

Что такое DSL?

Все языки программирования можно разделить на языки общего назначения и области применения. SQL, регулярные выражения, build.gradle часто приводятся в качестве примеров DSL. Эти языки ограничены в функциональности, но они способны эффективно решать определенную проблему. Они позволяют писать не императивный код (мы не должны объяснять, как решить проблему), а более или менее декларативным способом (мы просто объявляем задачу), чтобы получить решение на основе данных.

Допустим, у вас есть стандартное определение процесса, которое в конечном итоге можно изменить и улучшить, но обычно вы хотите использовать его с различными форматами данных и результатов. Создавая DSL, вы создаете гибкий инструмент для решения различных проблем в пределах одной предметной области, независимо от того, каким образом ваше решение получит конечный пользователь DSL. Таким образом, вы создаете своего рода API, который, если его освоить, может упростить вашу жизнь и упростить поддержание актуальности системы в долгосрочной перспективе.

Статья посвящена созданию встроенного DSL в Kotlin как языка, реализованного на синтаксисе языков общего назначения. Вы можете прочитать больше об этом здесь .

Область реализации

На мой взгляд, одним из лучших способов использования и демонстрации Kotlin DSL является тестирование.

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

Именно так мы долго работали в нашем проекте в области образования: мы использовали строителей и вспомогательные классы, чтобы покрыть тестами один из наших самых важных модулей – планирование школьного расписания. Теперь этот подход уступил языку Kotlin и DSL, который используется для описания тестовых сценариев и проверки результатов. Ниже вы можете увидеть, как мы воспользовались Kotlin, чтобы тестирование подсистемы планирования больше не было пыткой.

В этой статье мы углубимся в детали построения DSL, который помогает протестировать алгоритм построения расписаний учителей и учеников.

Ключевые инструменты

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

Kotlin DSL в автоматизации

Если вы занимаетесь UI-тестированием, то знаете, как порой нелегко выискивать истину среди множества локаторов, расположенных в длинной «портянке» автотеста. Конечно, если вы на проекте уже 2+ года, то вы даже среди ночи вспомните, что есть что. Но для новичка быстро въехать в происходящее — та еще задача. Решить эту проблему можно, перейдя на Kotlin DSL.

Вышеупомянутый подход на практике реализуется относительно просто. То есть если у вас были тесты, состоящие из одинаковых низкоуровневых вызовов — скроллов, кликов, ввода текста и пр., то посредством Kotlin DSL вы получите нечто вроде BDD-подхода, когда все видно и понятно, как и любит «бизнес».

Так ли это важно в принципе? Ну, все зависит от ситуации на проекте. Многие знакомы с историей, когда уходит один-единственный автоматизатор, и для тестов это заканчивается весьма плачевно — их просто перестают поддерживать должным образом, так как порог входа оказывается для большинства слишком высоким, ведь для понимания такого кода нужно много времени и соответствующая квалификация.

1-1801-845029.png

Kotlin DSL позволяет переделывать тесты так, чтобы в итоге вы смогли оперативно перебросить на автоматизацию нужное количество людей, причем как с других проектов, так и из ручного тестирования. То есть простейшие тесты на Kotlin DSL сможет писать почти любой. В результате автоматизаторам можно без проблем оставить низкоуровневую реализацию, а к написанию новых и простых тестов можно относительно быстро подключить функциональщиков. К тому же, функциональщики знакомы с бизнес-логикой. В данном случае Kotlin DSL позволит им описывать тест-кейсы так, как они хотят, оставляя за рамками низкоуровневую реализацию методов.

Пишем DSL в Koltin

Всем привет! Частенько зависаю на Medium и нахожу уйму полезных статей от зарубежных разработчиков. В один из таких дней искал для себя что-нибудь по DSL в Kotlin и наткнулся на серию статей о том, что такое DSL в Kotlin и как с этим работать. До прочтения я имел поверхностное понятие о DSL, так как совсем изредка сталкивался ними. Во время чтения статьи мне понравилась простота описания и подачи примеров от автора так, что по окончанию прочтения я решил перевести эту пару статей для вас. Разумеется, с одобрения автора 🙂 Ну что ж, начнём.

Кратко о DSLs. Что это?

Для начала можно подсмотреть определение из Википедии:

Domain-specific language (DSL) — компьютерный язык, специализированный для какой-либо конкретной области применения. Он отличается от языков общего назначения (GPL), которые широко применяются во многих областях.

В принципе, DSL — язык, который фокусируется на одной конкретной части приложения, с другой стороны, языки общего назначения, такие как Kotlin и Java, могут использоваться в других частях приложения. Есть несколько DSL, с которыми вы уже наверняка знакомы, например SQL. Если взглянуть на SQL, то можно заметить, что он выглядит почти как обычное предложение на английском языке, благодаря чему он становится вполне читаемым, понятным и выразительным:

SELECT Person.name, Person.age FROM Person ORDER BY Person.age DESC

Каких-то особенных критериев, которые бы отличали DSL от нормального API нет, но часто мы замечаем одно различие: Использование определённой структуры или грамматики. Это делает код более понятным для человека, который лёгок в понимании не только для разработчиков, но и для людей, которые менее подкованы в части языков программирования.

DSLs с Kotlin

Теперь давайте разберёмся, как мы можем создать DSL с некоторыми языковыми особенностями Kotlin и какие преимущества это нам приносит?

Когда мы создаём DSL на каком-то универсальном языке программирования, таком как Kotlin, то мы фактически имеем в виду внутренние DSL. Ведь мы не создаем независимый синтаксис, а просто настраиваем конкретный способ использования данного языка. И именно это даёт нам преимущество использования кода, который мы уже знаем, и позволяет нам добавлять разные операторы, такие как циклы for, в наш DSL.

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

  • Использование лямбд вне скобок метода
  • Лямбды с аргументами (приемниками)
  • Функции расширения (от переводчика: они же Extension functions, о которых уже все слышали, наверное)

Чтобы сделать всё максимально понятным, в этой части я буду использовать простую модель для создания нашего DSL. Мы не должны создавать DSL при создании класса. Это было бы лишним. Хорошим место для использования DSL может быть класс конфигурации или интерфейс библиотеки, где пользователь не должен знать о моделях.

А теперь давайте напишем наш первый DSL

В этой части мы создадим простой DSL, который сможет создать объект класса Person. Заметьте — это всего лишь просто пример. Итак, вот пример того, что мы собираемся получить в конце этого урока:

val person = person < name = "John" age = 25 address < street = "Main Street" number = 42 city = "London" >> 

Можно сразу заметить, что код выше сам по себе описывает себя и лёгок в понимании. Даже тот человек, который не имеет опыта разработчика, сможет прочитать это и даже внести свои правки. Чтобы понять, как мы сможем такое воссоздать, мы совершим несколько шагов. Вот модель, с которой мы начнём:

data class Person(var name: String? = null, var age: Int? = null, var address: Address? = null) data class Address(var street: String? = null, var number: Int? = null, var city: String? = null) 

Очевидно, что это не самая чистая модель, которую мы можем написать. Но мы хотим иметь иммутабельные проперти (val). И до этого мы доберемся в следующих частях этой серии.

Первое, что мы сделаем — создадим новый файл. В нём будем держать DSL отдельно от фактических классов в нашей модели. Начнём с создания какой-нибудь функции-конструктора для нашего класса Person. Смотря на результат, который мы хотим иметь, мы видим, что все проперти класса Person определены в кодовом блоке. Но на самом деле этим фигурные скобки означают лямбду. Здесь мы и используем первую из трех вышеперечисленных особенностей языка Kotlin: Использование лямбд вне скобок метода.

Если последним параметром функции является лямбда, то мы можем использовать её вне скобок. А если у вас только один параметр, который является лямбдой, то вы можете и вовсе удалить скобки. Это значит, что person тоже самое, что и person(<. >). Это приводит к меньшему синтаксическому загрязнению в нашем DSL. Теперь напишем первую версию нашей функции.

fun person(block: (Person) -> Unit): Person

Итак. Здесь у нас функция, которая создает объект Person. Для этого требуется лямбда, у которой есть объект, который мы создаём в строке 2. Когда мы выполняем эту лямбду в строке 3, то мы ожидаем, что объект получит необходимые ему проперти, прежде чем мы вернём объект в строке 4. А теперь давайте посмотрим, как мы можем использовать написанную нами функцию:

val person = person

Поскольку лямбда получает только один аргумент, то мы можем обращаться к объекту person через it. Это выглядит довольно таки неплохо, но это ещё не конец. На самом деле это не совсем то, что мы хотим видеть в нашем DSL. Особенно, когда мы собираемся добавить дополнительные слои объектов. Это приводит нас к следующей упомянутой нами функции Kotlin: Лямбды с аргументами (приемниками).

В определении функции person мы можем добавить приемник к лямбде. Таким образом, мы можем получить доступ только к функциям этого приемника в данной лямбде. Поскольку функции входят в область получателя, то мы можем просто выполнить лямбду на приемнике вместо того, чтобы подставлять его его в качестве аргумента для лямбды.

fun person(block: Person.() -> Unit): Person

А ещё это можно написать попроще. Например, с помощью функции apply, предоставленной Kotlin.

fun person(block: Person.() -> Unit): Person = Person().apply(block) 

Теперь мы можем убрать it из нашего DSL.

val person = person

Выглядит замечательно, не так ли? 🙂 Мы почти закончили. Но мы упустили один момент — класс Address. В нашем желаемом результате он очень похож на функцию person, которую мы только что создали. Единственное различие здесь в том, что мы должны назначить его как проперти address для объекта Person. Для того, чтобы сделать это, мы воспользуемся последним из трёх упомянутых функций языка Kotlin: Функции расширения.

Функции расширения дают вам возможность добавлять функции к классам без доступа к исходному коду самого класса. Это идеально подходит для создания объекта Address и непосредственно присваивает его для проперти address в Person. Вот окончательная версия нашего файла DSL (на данный момент).

fun person(block: Person.() -> Unit): Person = Person().apply(block) fun Person.address(block: Address.() -> Unit)

Мы добавили функцию address к Person, которая принимает лямбду с Address в качестве приемника, точно так же, как мы делали это с функцией-конструктором person. Затем она устанавливает созданный объект Address в проперти класса Person. Теперь мы создали DSL для создания нашей модели.

val person = person < name = "John" age = 25 address < street = "Main Street" number = 42 city = "London" >> 

Это первая часть из длинной серии о том, как писать DSLs в Kotlin. Во второй части мы поговорим о добавлении коллекций, использовании шаблона Builder и аннотации @DslMarker. Существует пример из реальной жизни с использованием GsonBuilder.

Оригинал статьи вы можете найти здесь: Writing DSLs in Kotlin (part 1)
Автор оригинальной серии статей: Fré Dumazy

  • kotlin
  • dsl
  • domain-specific language

Записки программера

Вот и пришла пора для заключительной статьи в основном цикле о Kotlin. Что это значит? Пользуясь знаниями из предыдущих статей (Введение, Незнакомые конструкции, Мигрируем из Java, Неловкие моменты) ниже мы с вами напишем собственный DSL (domain-specific language), обсудим что это такое и чем Kotlin, как язык, способствует написанию предметно-ориентированных языков, а также как это упрощает разработчикам жизнь, я расскажу о собственном опыте в разработке DSL и о проблемах, которые возникают. Статья может показаться довольно большой, так и есть, я постарался уместить в неё основной концентрат собственных знаний о построении DSL в Kotlin.

Что такое DSL?

Языки программирования можно разделить на 2 типа: универсальные языки (general-purpose programming language) и предметно-ориентированные (domain-specific language). Популярные примеры DSL это SQL или регулярные выражения. Язык уменьшает объем функциональности который, он дает, при этом он способен эффективно решать определенную проблему. В том числе это способ описать программу не в императивном стиле, когда вы говорите как нужно получить результат в универсальны языках, а в декларативном, когда вы говорите что вы хотите получить. Пусть у вас есть стандартный процесс выполнения, который иногда может меняться, дорабатываться, но в целом вы хотите подстраивать его под разные данные и формат результата. Создавая DSL вы делаете гибкий инструмент для решения различных задач из одной предметной области при этом не задумываетесь об оптимизации. Это некоторое API, виртуозно пользуясь которым, вы можете сильно упростить себе жизнь и долгосрочную поддержку системы.

«Чистое API»

«Чистым» называется API, которое требует минимальных действий для получения результата, никакой мишуры и фантиков. Давайте рассмотрим, что нам дает Kotlin для построения такого API.

На пути к совершенству

Давайте перечислим основные преимущества Kotlin, которые позволяют достаточно чисто писать на этом языке и доступны для построения собственного DSL. Ниже представлена табличка в которой перечислены основные улучшения синтаксиса языка, которые стоит использовать

Благодаря этим возможностям вы способны писать код чище, избавиться от множества вызовов методов и при этом сделать разработку еще более приятным занятием («куда уж приятнее?» — спросите вы). Мне понравилось сравнение из книжки, что в натуральных языках, например, в английском, предложения построены из слов и граматические правила управляют тем как нужно объединять слова друг с другом. Аналогично в DSL, одна операция может быть сложена из нескольких вызовов методов, а проверка типов обеспечит гарантию, что конструкция имеет смысл.

Оказывается, что довольно трудно совместить одновременно универсальный язык и DSL, стереть границу которая их разделяет. Прикрутить что-то к чему-то можно всегда, но как это сделать, чтобы работа с таким DSL была максимально простой и нативной? Давайте обсудим «внутренние DSL» и как они стирают интеграцию.

Внутренний DSL

Проще всего понять что такое «внутренний» DSL на примере. Вот у вас есть реляционная база данных и мы хотим выбрать из неё определенные записи, как мы это делаем? Очевидно, SQL. У нас будет select запрос в котором мы укажем что и как нужно достать. В перевес этому мы можем взять фреймворк Exposed на Kotlin (не то чтобы я сторонник миллионов оберток, но это хороший пример). Exposed — это работа с базой данных в sql подобном стиле прямо из кода. Больше подробностей по фреймворку по ссылке в конце статьи.

Пример DSL

Предлагаю посмотреть, а ради чего всё это. Я приведу пример из практики. Библиотека Clabo для создания телеграм ботов в декларативном стиле, демонстрирует возможности, которые дает Kotlin для построения DSL. Каждый бот имеет свой ключик, который выдается при создании бота для управления им. При работе с ботом нам нужно при каждом запросе указывать этот ключик в URL в качестве авторизации. Long Pooling боты основываются на повторных подвисающих запросах на серверах telegram. Чтобы обработать сообщения или другие события, нужно запросить у сервера последние изменения, затем правильно распознать, например, команду и обработать её. Сформировать ответное сообщение и отправить его обратно. Оценили количество действий? А теперь давайте посмотрим как эту тонну стандартных действий можно спрятать за DSL.

fun main(args: Array) < bot("apiKey") longPool < onStart < command ->command.message reply "Hi! I'm Bot" > > >

Семь строчек кода без импортов и ваш бот уже в строю. Из перечисленных выше преимуществ здесь использованы infix функции reply и longPool, а также лямбды обработчиком за скобками.

Лямбда — украшение стола

Выше были перечислены различные фичи, которые помогают в построении DSL. Давайте рассмотрим их в порядке роста важности (на мой взгляд).

Переопределение операторов

Об этом я немного говорил в предыдущих статьях, но напомню. В Kotlin есть модификатор на функции operator , благодаря нему мы можем определять или переопределять смысл операторов, используя функции, рассмотрим, например, инкремент и декремент.

Основная суть в том, что в операторах нет ничего сложного, это обычная функция со стандартизированным названием, которая вызывается при вызове оператора.

Еще больше информации по операторам вы найдете по ссылке в конце статьи. Изначально я хотел привести здесь их все со своими комментариями, но на деле документация на столько хороша, что это не требуется.

Как это помогает?

Всё строго зависит от предметной области, если ваша структура подразумевает действия, которые можно сократить до операторов, то вперёд. Но не переусердствуйте, здесь важно чувствовать баланс. Вы легко можете сделать код более сложным, если очередной «плюс» выполняет совершенно не очевидные вещи.

Соглашение для get методов

Очень крутая штука и сильно сокращает код, особенно если в разработке вам нужно «гулять» по структурам типа map, то это поможет очень хорошо. Аналогично с фичей выше это оператор. Взглянем как определить свою реализацию разных доступов по индексам/ключам.

Как мы видим сокращенная форма превращается в вызов обычных методов, но нас этим уже не удивить. Интересно, что существует множество вариаций.

Делегированные свойства

Один из наиболее популярных примеров делегированных свойств — ленивая инициализация. Фактически, одной строкой мы можем описать, что поле должно проинициализироваться один раз, а затем возвращаться это значение. В Kotlin из-за наличия nullability мы не можем говорить, что свойство notnull, а её backing field (мы их рассматривали в прошлой статье) — nullable. По этому, для сохранения notnull типа свойства от нас требуется заводить дополнительную nullable переменную, выглядит всё это следующим образом.

class UserComponent < private var _hugeData: HugeData? = null val hugeData: HugeData get() < if(_hugeData == null) < _hugeData = loadHugeData() >return _hugeData!! > fun loadHugeData(): HugeData < return HugeData().apply < //load and fill huge data >> >

Естественно, это отвратительно и да, это можно исправить. На помощь приходят делегированные свойства. Весь код выше можно заменить на код ниже, он делает то же самое.

class UserComponent < val hugeData by lazy < loadHugeData() >fun loadHugeData(): HugeData < return HugeData().apply < //load and fill huge data >> >

Ключевое слово by используется для прикрепления делегата. Что же это означает? Создается специальный объект, который имеет метод getValue к которому переадресовываются все вызовы. Это и есть делегат.

Давайте учиться на существующий примерах. lazy — это first-class функция, которую можно вызвать везде в проекте. В эту функцию можно передать лямбду, которую в Java 8 мире принято называть Supplier’ом. Внутри этой функции мы видим как создается объект SyncronizedLazyImpl . Благодаря этому теперь мы знаем, то что есть 3 разных вида Lazy Property, которые регулируются параметром с типом LazyThreadSafetyMode . Внутри SyncronizedLazyImpl мы увидим свойство value. Вот она реализация lazy. Но не хватает метода getValue, где же он? У него должна быть определенная сигнатура и мы находим его в том же файле в виде Extention функции. Объявляется он вот так

operator fun Lazy.getValue(thisRef: Any?, property: KProperty): T

Т.е. кто угодно унаследованный от Lazy может быть использован как делегат. Проверим? Конечно!

Хочу сразу сказать, что наследоваться от Lazy — не обязательное условие, вы можете просто объявить эту функцию (getValue) в своем классе. Давайте так и сделаем завернем загрузку properties из файла в делегат и пусть при первом обращении мы будем загружать, а затем использовать. Создадим файл для демо. Это простой .property файл в котором только одна запись key=value . Разберем на код ниже

fun main(args: Array) < val component = UserComponent("D://someProperties.properties") println("Component is initialized") val keyProperty = component.properties["key"] println(keyProperty) >class UserComponent(propertiesPath: String) < val properties by LazyPropertyLoader(propertiesPath) >class LazyPropertyLoader(val filePath: String) < private var properties: Properties? = null operator fun getValue(thisRef: Any?, property: KProperty): Properties < if (properties == null) < properties = Properties() properties. load(FileInputStream(filePath)) println("Property is loaded") >return properties!! > >

В методе main мы создаем компонент, который хранит эти Properties, а класс LazyProperyLoader это делегат, который работает благодаря методу getValue. Да, эта реализация потоко-небезопасна, но пример очень прост и компактен. Теперь когда у объектов с типом UserComponent будет запрашиваться свойство properties, на самом деле вызов будет переадресовываться к getValue и в свою очередь лениво тянуть поле properties.

Всё очевидно и просто, здесь и ленивая загрузка и делегат, в общем всё что нужно! Ничего сложного.

Псевдонимы типа

Очередная сахарная фича, которая не заставит долго ждать применения это псевдонимы типов. Сразу приведем пример

typealias Point = Pair

Такой псевдоним не вводит новый тип, но он эквивалентен вызову пары, первая цель этой функциональности — уменьшить длинные декларации типов. Как вы думаете, почему в популярной библиотеке Google Guava нет класса «пара»? Думаете не досмотрели? Как бы не так! Этот класс упрощает жизнь разработчику, но усложняет её тому, кто будет читать код, т.к. стирается весь смысл связи двух объектов. В Kotlin есть стандартный класс пары и когда мы хотим добавить ему смысла мы легко можем добавить псевдоним. Еще один хороший пример — Telegram API. Если вы когда либо разрабатывали ботов для телеграм, то вы отлично знаете, что есть id чата, id сообщения, id кучи чего еще и хорошо бы наделить смыслом сухой String. Мне нравится делать следующим образом, когда мне нужно объявить тип очередного идентификатора, я смело указываю его псевдоним, например

val processor: (ChatId, MessageId) -> Unit = < chatId, messageId ->. >
val processor: (String, String) -> Unit = < chatId, messageId ->. >

Лямбда вне скобок

Как вы знаете, JetBrains не переизобретали язык программирования, а честно позаимствовали лучшие практики. Благодаря этому появился хороший, практичный язык программирования Kotlin. Одна из заимствованных возможностей это вынесение лямбды за скобки. Это довольно просто, если лямбда является последним параметром, то её можно вынести за скобки, а если других параметров нет, то скобки можно удалить. Пример ниже

fun run(lambda: () -> Unit) < lambda() >fun parametrizedRun(param: String, lambda: (String) -> Unit) < lambda(param) >fun invoke() < run < println("no param") >parametrizedRun("param") < println(it) >parametrizedRun("param") < param ->println(param) > >

Давайте разберемся. Внутри функции invoke вызывается функция run у которой первый и единственный параметр лямбда. Это тот случай когда мы можем избавиться от круглых скобок. Затем вызывается parametrizedRun , в который нужно передать параметр. Здесь от скобок нам не избавиться никак, но обратите внимание, что по умолчанию, от нас не требуют писать название передаваемого параметра и мы можем использовать название по умолчанию it . Прошу заметить, что если у вас несколько параметров, то проигнорировать их имена, как здесь, не удастся, однако вы можете использовать специальный символ _ , благодаря которому вам не придется писать имена параметров, которые вы не используете. Эта возможность появилась в релизе языка под номером 1.1 . Наконец, если вы всё-таки хотите именовать свой параметр, то вам никто не запрещает и на этот случай есть третий пример.

Extention функция

Иногда нужно прикрутить один не сложный метод к классу, которого очень не хватает, а класс из библиотеки и изменить его не вариант или мы хотим сделать более читаемой обработку какого-то объекта и при этом не тащить эту логику в него, в таких ситуациях помогают extention функции. Фактически, это функции, которые крепятся к какому либо классу, интерфейсу или даже обобщенному типу, а затем могут быть вызваны в любом месте где идет обработка инстансов. На деле, такие функции не встраиваются в класс и не получают доступ к приватному API, а всего лишь работают с тем, что доступно всем остальным. Объявляются они очень просто fun ClassName.funName(. ) . Здесь вы можете использовать дженерики или просто указать интерфейс, компилятор позволит использовать метод только там, где это возможно в соответствии с описанной сигнатурой метода.

Infix функции

Пожалуй, одна из самых сладких фич, благодаря которой вызовы функций выглядят как предложения на естественном языке — infix функция. Такая функция применяется для двух параметров, при чем первый — это контекст вызова функции, т.е. некоторый объект на котором она будет вызвана, а второй параметр передается в функцию. Эта фича замечательно сочетается с лямбдой за скобками и extention функцией. Сначала рассмотрим простой пример. Ранее мы ввели псевдоним Point, пришло время им воспользоваться. Напишем infix функцию для перемещения игрока по клеточкам

class Player(var position:Point = Point(0, 0)) < infix fun moveTo(point: Pair) < position = point >>

Готово! Модификатор infix позволяет нам использовать функцию следующим образом

val player = Player() player moveTo Point(1, 1)

Не потрясающе ли? Никакого шума в виде точек или скобок! Но это превращается в нечто более интересное, если мы совместим лямбду за скобками с infix функцией. Декларация этого выглядит так:

class Player(var position:Point = Point(0, 0)) < infix fun processPosition(processing: (Point) ->Unit) < processing(position) >>

Мы передаем лямбду в метод processPosition и тут же её вызываем. Единственный параметр в лямбде — Point. В итоге применение такого метода выглядит следующим образом

val player = Player() player processPosition < println("X:$Y:$") >

Как можно улучшить этот пример? Давайте взглянем на деструктирующую декларацию

Деструктирующая декларация

Страшно звучит, не хочу это литературно переводить — ни к чему. В прошлых статьях я писал о модификаторе data и о том, что он добавляет к классу методы componentN. Фактически, каждый такой метод, component1, component2 и т.д. говорит о том каким по счету вернется возвращаемый параметр при «разбирании» объекта на набор полей. Представим, что у нас есть Point, который, как мы знаем, Pair, и у него есть 2 параметра в первичном конструкторе и модификатор data, это значит, что для такого класса будет сгенерировано 2 метода component1 и component2. Для примера продемонстрируем следующее сравнение

val point = Point(1, 1) val (x, y) = point

этот код равносилен следующему

val point = Point(1, 1) val x = point.component1() val y = point.component2()

Такое «подслащение» дает возможность обновить предыдущий пример вызова infix функции до такого

val player = Player() player processPosition < (x, y) ->println("X:$x Y:$y") >

Код стал чуть более читаемым. По крупицам мы почти собрали джентельменский набор для построения DSL. Жемчужина этого набора — лямбда с обработчиком.

Лямбда с обработчиком

Переиспользование контекста, пожалуй, одна из самых крутых фич, о её проблемах чуть позже, а пока просто похвалим. В Kotlin’e мы можем не просто вызывать лямбды, а еще и относительно какого-то контекста, очевидный пример — функция apply. Подглянем в её декларацию

fun T.apply(block: T.() -> Unit): T

Что мы здесь видим? Во первых, это уже знакомая нам extention функция на не ограниченный дженерик, что означает, её можно вызывать где угодно. Единственным параметром является лямбда, но не обычная, а с обработчиком. Я говорю о вот такой декларации

T.() -> Unit

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

Player().apply

При печати мы увидим вывод метода toString класса Player. «Почему же это жемчужина?» — спросите вы. Всё просто! В совокупности с остальными методами на основе таких лямбд строится большая часть DSL, например, мы хотим создавать игроков определенной игры и код может выглядеть, например, так

data class Player(var name: String? = null) class Game(val players: MutableSet = HashSet()) infix fun Game.newPlayer(init: Player.() -> Unit) < val player = Player() player.init() players += player >fun main(args: Array) < val game = Game() game newPlayer < name = "Иванов" >println(game.players) >

Здесь мы видим использование infix функции, лямбды с обработчиком за скобками и использование оператора. Важно понимать, что лямбда с ресивером вызывается на определенном контексте и по этому мы не можем сделать init() , а делаем player.init()

Резюме по фичам

Каждая фича в отдельности не плоха, но вместе они порождают гремучую смесь, это самая настоящая синергия.

Мой опыт

Прежде всего, хочу сказать, что в познании Kotlin мне очень помогла книжка, Kotlin In Action, хотя изучить язык вы сможете и без неё, документация у Koltin хорошая, но это книга дает более глубокое, если позволите, понимание философии языка и целостное повествование. Вторым источником знаний для меня было множество видео на YouTube. Я не считаю себя глубоким экспертом, но мне нравится к этому стремиться и эти статьи тоже часть пути.

Первый опыт использования Kotlin пришелся на рабочий проект, а именно на интеграционное тестирование. Передо мной стояла задача покрывать тестами разрастающийся (моими же силами) модуль проекта. Модель данных росла, количество связей увеличивалось и первое время у меня был специальный класс, который помогал генерировать любую сущность для тестов, это было очень удобно и достаточно. Тот самый случай, когда использование паттернов проектирования скорее навредило бы. Однако, в определенный момент, я понял для себя, что тесты могут выглядеть сильно лучше и попробовал Kotlin. Это было не плохо, но делать всё то же самое на другом языке мне не хотелось и я, вместе с коллегой, которому понравилась эта идея, принялся за проектирование DSL. Проектирование — довольно важный этап, т.к. без него получится скорее всего что-то не очень хорошее и низко качественное. Пожалуй, решение воспользоваться DSL было одним из лучших в тот момент, т.к. сегодня я легко поддерживаю старые, пишу новые тесты и это не составляет какого-либо труда и приносит удовольствие, когда пишешь на языке и не выкручиваешь себе руки.

Получается неплохо, но есть проблемы о которых ниже в перечне минусов.

Для желающих добавить Kotlin в рабочий проект

Прежде всего, будьте готовы к тому что любая инициатива имеет свои последствия. Мне не нравится фраза «Инициатива наказуема» и её аналоги. Я сторонник того, что если можешь сделать стратегически лучше — сделай. Если вы чувствуете в себе силы отвечать за последствия, а это означает любую поломку так или иначе связанную с языком, то делайте. Для начала вы можете привнести Kotlin для тестирования, а затем постепенно захватывать проект 🙂

Минусы DSL

Не бывает всё идеально, и на этой полянке растут свои поганки. Давайте их обсудим.

Переиспользование части DSL

Что если вы хотите переиспользовать часть своего DSL? Что если внутри переиспользуемой части используется множество различных контекстов? Встает вопрос: «Как это сделать?». Возможно вы подскажете варианты лучше моих, но мне известно два работающих пути: добавлять «именованные callback’и», как часть DSL или плодить лямбды (вернее замыкания). Второй вариант проще, но его последствия могут вылиться в самый настоящий ад, когда вы пытаетесь отследить последовательность и голова уже не способна понять. Что было вызвано? В каком контексте? Решение проблемы простое, там где нужна большая вложенность разбейте на большее количество лямбд с говорящим названием. Почему здесь не подходят функции? Вот она, причина

val lambda = < fun a() < if(Random().nextBoolean()) < b() >> fun b() < if(Random().nextBoolean()) < a() >> >

Этот код не компилируется, так как внутри функции a не видно функции b это можно исправить поменяв их местами, но тогда внутри функции b не будет видно a . По этой причине функции нам не подходят, но при этом остаются лямбды, декларация которых находится вне DSL и к ним мы имеем полный доступ.

This, it!?

Очень легко потерять смысл текущего this и it. Там где вам нужен it, пожалуйста, пишите осмысленное название для параметра, эта пара секунд упростит вам жизнь в будущем. С this вы тоже можете выйти сухим из воды. Там, где появляется запутанность, используйте другую ссылку на this, объявленную в том месте, где нет двусмысленности или она минимальная

val mainContext = this

Вложенность

Этот минус тесно переплетен с первым. Когда вы используете вложенные во вложенные во вложенные конструкции, то ваш код внутри начинает двигаться вправо и единственное, что здесь можно сделать, это следовать совету — разбивать на разные лямбды, естественно, такой подход уменьшает читаемость DSL, но это некоторый компромисс, в том случае, когда DSL подразумевает не только создание структур, но и какую-то логику. Например, в случае написания тестов на DSL (о чем я писал выше) этой проблемы нет, т.к. данные описываются компактными структурами, а в случае с ботом, основанным на DSL — крайне желательно переиспользовать код и это необходимая мера.

Где доки, Зин?

Если DSL делал кто-то другой, то встает вопрос: «Где доки. «. На этот счет у меня есть свое мнение. Если вы пишите DSL, который будете использован не только вами, то лучшей документацией будут его примеры использования, которые делают именно то, что от них ожидает наблюдатель. Сама по себе документация важна, но скорее как дополнительная справка и смотреть её зачастую не очень удобно, т.к. наблюдатель задается вопросом «Что мне нужно вызвать, чтобы получить результат?» и здесь эффективнее всего будут примеры схожего использования.

Заключение

Подошла к концу пятая часть цикла статей о Kotlin. Долгий путь мы проделали вместе с вами, я постарался передать вам всю атмосферу языка, всю его сладость, показать яркие моменты, рассказать о возникающих проблемах честно и без утайки. Если эти статьи замотивировали вас попробовать язык или хотя бы перестать его бояться, то я очень рад, значит всё это было не в пустую. На этом базовый цикл статей о Kotlin заканчивается и все остальные будут являться скорее приложениями о применении языка. Спасибо, читатель, благодаря тебе я поглубже нырнул в Kotlin, заглянул под капот и развеял некоторые собственные заблуждения.

Ссылки, которые обещал:

Справка по операторам здесь

Эту статью я подготовил на основе собственного опыта и материалов из книги Kotlin In Action.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *