kotlin dsl что это

Пишем DSL в Koltin

Небольшое вступление

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

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

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

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

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

DSLs с Kotlin

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Источник

DSL для регулярных выражений на Kotlin

Эта статья про реализацию одного конкретного DSL (domain specific language, предметно-ориентированный язык) для регулярных выражений средствами Kotlin, но при этом она вполне может дать общее представление, о том, как написать свой DSL на Kotlin и что обычно будет делать «под капотом» любой другой DSL, использующий те же возможности языка.

Многие уже используют Kotlin или хотя бы пробовали это делать, да и остальные вполне могли слышать о том, что Kotlin располагает к написанию изящных DSL, чему есть блестящие примеры — Anko и kotlinx.html.

Конечно же, для регулярных выражений подобное уже делали (и ещё: на Java, на Scala, на C# — реализаций много, похоже, это распространённое развлечение). Но если хочется попрактиковаться или попробовать DSL-ориентированные языковые возможности Kotlin, то добро пожаловать под кат.

Как обычно выглядит DSL, написанный на Kotlin?

В худшем случае, наверное, так.

Большинство DSL на Java предлагают использовать цепочки вызовов для своих конструкций, как в этом примере Java Regex DSL:

Мы могли бы использовать этот подход, но он имеет ряд неудобств, среди которых можно сразу отметить два:

С этими недостатками в Kotlin можно справиться, если реализовать DSL в стиле Type-Safe Groovy-Style Builder (при объяснении технических деталей эта статья будет во многом повторять страницу документации по ссылке). Тогда выглядеть код на нём будет подобно этому примеру Anko:

Или этому примеру kotlinx.html:

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

Приступим

Зачем нужен RegexContext?

Соответственно, функция regex <. >теперь будет выглядеть следующим образом:

Следующие функции, если явно не сказано обратного, тоже расположены в теле класса.

Всё очень просто

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

Читайте также:  Что значит сюжет картины

А вот для добавления произвольной строки в регулярное выражение придётся её сначала преобразовать, чтобы присутствующие в строке символы не интерпретировались как служебные. Самый простой способ это сделать — с помощью функции Regex.escape(. ):

Идём глубже

Что насчёт квантификаторов? Очевидное наблюдение: квантификатор навешивается на подвыражение, которое само по себе тоже валидный регекс. Давайте добавим немного вложенности!

Мы хотим вложенным блоком кода в фигурных скобках задавать подвыражение квантификатора, примерно так:

И потом используем их для реализации наших «квантификаторов»:

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

А сами функции объявим так:

Сгруппируйтесь!

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

Однако группы вносят новую сложность в структуру регекса: они нумеруются «насквозь» слева направо, игнорируя вложенность подвыражений. А значит, нельзя считать вызовы group <. >независимыми друг от друга, и даже больше: все наши вложенные подвыражения теперь тоже друг с другом связаны.

Чтобы поддерживать нумерацию групп, слегка изменим RegexContext : теперь он будет помнить, сколько групп в нём уже есть:

И, чтобы наши вложенные контексты знали, сколько групп было до них и сообщали, сколько добавилось внутри них, изменим функцию pattern(. ) :

Теперь нам ничего не мешает корректно реализовать group :

Случай именованных групп:

И матчинг групп, как индексированных, так и именованных:

Что-то ещё?

Да! Мы чуть не забыли важную конструкцию регулярных выражений — альтернативы. Для литералов альтернативы реализуются тривиально:

Не сложнее реализация для вложенных выражений:

Если это аккуратно сделать, оно даже заработает, но мы неожиданно упрёмся в неприятный момент в грамматике Kotlin: в цепочке вызовов оператора invoke нельзя использовать подряд вызовы с фигурными скобками.

Ну, и нужно оно нам такое?

И на этом, пожалуй, обязательные фичи заканчиваются.

Заключение

Спасибо, что дочитали до конца! Что у нас получилось? Вполне жизнеспособный DSL для регексов, которым можно пользоваться:

(Вопрос: для чего этот регекс? Правда же, он простой?)

Исходники, тесты и готовая к добавлению в проект зависимость — в репозитории на Github.

Что вы думаете по поводу предметно-ориентированных языков для регулярных выражений? Пользовались ли хоть раз? А другими DSL?

Источник

Пишем простой DSL на Kotlin в 2 шага

DSL (Domain-specific language) — язык, специализированный для конкретной области применения (Википедия)

На написание этого поста меня натолкнула статья «Почему Kotlin отстой», в которой автор сетует на то, что в Kotlin «нет синтаксиса для описания структур». За некоторое время программирования на Kotlin у меня сложилось впечатление, что в нём если нельзя, но очень хочется, то можно. И я решил попробовать написать свой DSL для описания структуры данных. Вот что из этого получилось.

Disclaimer

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

Синтаксис

Для проcтоты ли, или из-за каких-то личных предпочтений, я хочу, чтобы синтаксис моего будущего DSL для описания структуры данных был похож на JSON. Если коротко, синтаксис подразумевает следующее:

Шаг 0. Сначала была пустота

С чего-то надо начинать и начнем мы с того, что заставим компилироваться пустую структуру вида:

Сделать это совсем не сложно, нужно лишь объявить функцию

Функция struct(. ) принимает в качестве параметра другую функцию, возвращаующую Unit и пока больше ничего не делает. Но эта функция раскрывает нам важную фишку Kotlin, которая поможет нам в написании DSL: если последний аргумент функции – это другая функция, то её можно объявить за скобками «(. )». Если у функции всего 1 аргумент, и этот аргумент – функция, то круглые скобки можно не писать вообще.

Вот теперь у нас действительно есть какой-то пустой объект класса Struct

Шаг 1. Потом были данные

Пора бы добавить какое-то содержание. Я пытался найти способ заставить работать конструкцию вида

Точного совпадения мне добиться не удалось, зато получилось сделать аж 3 альтернативных синтаксиса, которые, при желании, можно использовать одновременно 🙂

Заметьте, что в третьем случае пришлось использовать круглые скобки, а не фигурные, зато в нём меньше всего символов.

Во-вторых, эти данные в структуру нужно как-то добавить. Напомню, что все, что находится внутри фигурных скобок после слова struct есть функция, которую мы передали в struct(. ) аргументом. Значит, чтобы манипулировать объектом Struct нам нужно получить доступ к этому объекту внутри переданной функции. И мы можем это сделать!

Для примера, теперь мы в праве писать такой код:

Это уже работает, но мало похоже на язык описания структуры данных. Добавим поддержу конструкции

Далее разберемся с поддержкой синтаксиса вида:

Здесь нет лямбд, сразу создание объекта Pair и какой-то символ «s» перед ней. На самом деле «s» — это тоже оператор, но уже инфиксный. Откуда он взялся? Так я сам его написал, вот он:

Он ничего не возвращает, но добавляет переданную ему пару в нашу структуру данных. Букву «s» я выбрал просто так, название оператора может быть любым. К слову, to в выражении «field1» to 1 это тоже инфиксный оператор, возвращающий пару Pair(«field1», 1)

Наконец, добавим поддержу третего варианта синтаксиса. Самого лаконичного, но самого скучного с точки зрения реализации.

Шаг 2. И получился DSL?

Да, это оператор «get» (именно оператор, а не геттер), который автоматически вызывается при обращению к объекту через квадратные скобки.

Итого

Можно сказать, что DSL у нас получился. Пусть не идеальный, с очевидными недостатками в виде невозможности автоматически вывести тип каждого поля, но получился. Вероятно, если попрактиковаться еще какое-то время, можно найти способы его улучшить. Может быть у читателей есть идеи?

Пример кода целиком можно посмотреть по ссылке

Источник

Kotlin DSL: Теория и Практика

Sql, RegExp, Gradle — что их объединяет? Всё это примеры использования проблемно-ориентированных языков или DSL (domain-specific language). Каждый такой язык решает свою узконаправленную задачу, например, запрос данных из БД, поиск совпадений в тексте или описание процесса сборки приложения. Язык Kotlin предоставляет большое количество возможностей для создания собственного проблемно-ориентированного языка. В ходе статьи мы разберемся, какие инструменты есть в арсенале программиста, и реализуем DSL для предложенной предметной области.

Весь синтаксис, представленный в статье, я объясню максимально просто, однако, материал рассчитан на практикующих инженеров, которые рассматривают Kotlin, как язык для построения проблемно-ориентированных языков. В конце статьи будут приведены недостатки, к которым нужно быть готовым. Используемый в статье код актуален для Kotlin версии 1.1.4-3 и доступен на GitHub.

Что такое DSL?

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

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

В статье я рассмотрел построение «внутреннего» DSL на языке Kotlin. Такой вид проблемно-ориентированных языков реализуется на основе синтаксиса универсального языка. Подробнее об этом вы можете прочитать по ссылке.

Область применения

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

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

Читайте также:  какой отпуск в средней полосе

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

Основные возможности

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

Название функциональности DSL синтаксис Обычный синтаксис
Переопределение операторов collection += element collection.add(element)
Псевдонимы типа typealias Point = Pair Создание пустых классов-наследников и прочие костыли
Соглашение для get/set методов map[«key»] = «value» map.put(«key», «value»)
Мульти-декларации val (x, y) = Point(0, 0) val p = Point(0, 0); val x = p.first; val y = p.second
Лямбда за скобками list.forEach list.forEach(<. >)
Extention функции mylist.first(); // метод first() отсутствует в классе коллекции mylist Утилитные функции
Infix функции 1 to «one» 1.to(«one»)
Лямбда с обработчиком Person().apply Нет
Контролирование контекста @DslMarker Нет

Нашли для себя что-то новое? Тогда продолжим.

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

Важно понимать, что в этой статье мы будем рассматривать «внутренний DSL», т.е. проблемно-ориентированный язык базируется на универсальном языке — Kotlin.

Пример финального результата

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

Инструменты

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

Некоторые возможности языка особенно хороши в совокупности с другими и первый инструмент в этом списке — лямбда вне скобок.

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

Ниже приведен простейший пример того, как можно сохранить лямбду в переменную:

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

В примере выше лямбда принимает один параметр. Внутри лямбды этот параметр по умолчанию имеет имя «it», но если параметров несколько, то вы должны явно перечислить их имена, либо использовать знак подчеркивания «_», чтобы проигнорировать его. Пример ниже демонстрирует такое поведение.

Базовый инструмент, который вы уже могли встретить, например, в Groovy, это лямбда вне скобок. Обратите внимание на пример в самом начале статьи, практически каждое использование фигурных скобок, за исключением стандартных конструкций — это использование лямбд. Существует как минимум два способа сделать конструкцию вида x < … >:

или в сокращенной форме для однострочных функций, вы можете записать так:

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

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

Kotlin предоставляет широкий, но ограниченный спектр операторов. Модификатор operator позволяет определять функции по соглашениям, которые будут вызываться при определенных условиях. Очевидным примером является функция plus, которая будет выполнена, при использовании оператора «+» между двумя объектами. Полный перечень операторов вы найдете по ссылке выше в документации.

Рассмотрим чуть менее тривиальный оператор «invoke». Главный пример этой статьи начинается с конструкции schedule < >. Назначение конструкции — обособить блок кода, который отвечает за тестирование планирования. Для построения такой конструкции используется способ, немного отличающийся от рассмотренного выше: оператор invoke + «лямбда вне скобок». После определения оператора invoke нам становится доступна конструкция schedule(. ), при том, что schedule — это объект. Фактически, вызов schedule(. ) интерпретируется компилятором как schedule.invoke(…). Давайте посмотрим на декларацию schedule.

Нужно понимать, что идентификатор schedule отсылает нас к единственному экземпляру класса schedule (синглтону), который помечен специальным ключевым словом object (подробнее о таких объектах, можно прочитать здесь). Таким образом, мы вызываем метод invoke у экземпляра schedule и при этом единственным параметром метода определяем лямбду, которую выносим за скобки. В итоге, конструкция schedule <… >равносильна следующей:

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

то для лямбды с контекстом нужен контекст:

Напомню, что в объекте schedule у нас определен оператор invoke (см. предыдущий параграф), который позволяет нам использовать конструкцию:

Лямбда, которую мы используем, имеет контекст типа SchedulingContext. В этом классе определен метод data. В результате у нас получается следующая конструкция:

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

Чтобы детально понять как работает этот пример, давайте уберем весь синтаксический сахар:

Как вы видите, всё предельно просто.
Давайте взглянем на реализацию оператора invoke.

В последних примерах мы рассмотрели оператор invoke и его взаимодействие с другими инструментами. Далее мы сфокусируемся на другом инструменте, который формально является оператором и делает наш код чище, а именно на соглашении для get/set методов.

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

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

Чтобы использовать квадратные скобки, необходимо реализовать методы get или set в зависимости от того, что нужно (чтение или запись) с модификатором operator. Пример реализации этого инструмента вы можете найти в классе Matrix на GitHub по ссылке. Это простейшая реализация обертки для работы с матрицами. Ниже часть кода, которая интересует нас.

Типы параметров функций get и set ограничены только вашей фантазией. Вы можете использовать как один, так и несколько параметров для get/set функций и обеспечивать комфортный синтаксис для доступа к данным. Операторы в Kotlin привносят много интересных возможностей, с которыми вы можете ознакомиться в документации.

К удивлению, в стандартной библиотеке Kotlin есть класс Pair, но почему? Большая часть сообщества считает, что класс Pair — это плохо, с ним пропадает смысл связи двух объектов и становится не очевидно, почему они в паре. Следующие два инструмента демонстрируют, как можно и осмысленность пары сохранить, и не создавать лишние классы.

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

Фактически, это обычное переименование конструкции. Благодаря такому подходу, нам не нужно создавать класс Point, который в данном случае просто дублировал бы пару. Теперь, мы можем создавать точки следующим образом:

Однако у класса Pair есть два свойства, first и second, и как бы нам переименовать эти свойства так, чтобы стереть всякие различия между желаемым классом Point и Pair? Сами свойства переименовать не удастся, но в нашем инструментарии есть замечательная возможность, которую народные умельцы обозначили как мульти-декларации.

Мульти-декларации (Destructuring declaration)

Для любого класса мы можем определить оператор componentN, который будет предоставлять доступ к одному из свойств объекта. Это означает, что вызов метода point.component1 равносилен вызову point.first. Теперь разберемся, зачем нужно это дублирование.

Что такое мульти-декларации? Это способ «разложить» объект по переменным. Благодаря этой функциональности, мы можем написать следующую конструкцию:

что в свою очередь равносильно:

где first и second это свойства объекта Point.

Конструкция for в Kotlin имеет следующий вид, где x последовательно принимает значения 1, 2 и 3:

Обратим внимание на блок assertions в DSL из основного примера. Для удобства часть его я приведу ниже:

Теперь всё должно быть очевидно. Мы перебираем коллекцию scheduledEvents, каждый элемент которой раскладывается на 4 свойства, описывающие текущий объект.

Extension функции

Добавление собственных методов к объектам из сторонних библиотек или добавление методов в Java Collection Framework — давняя мечта многих разработчиков. И теперь у всех нас есть такая возможность. Объявление расширяющих функций выглядит следующим образом:

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

Но, к сожалению, ничего с этим поделать нельзя, кроме как не использовать инструмент или добавлять методы только в определенный класс контекста. Тогда магия появляется только там, где она нужна. Более того, вы можете расширять этими функциями даже интерфейсы. Хорошим примером будет метод first, расширяющий любой Iterable объект следующим образом:

В итоге, любая коллекция, основанная на интерфейсе Iterable, вне зависимости от типа элемента, получает метод first. Интересно то, что мы можем поместить extension метод в класс контекста и благодаря этому иметь доступ к расширяющему методу только в определенном контексте (см. выше лямбда с контекстом). Более того, мы можем создавать extension функции и для Nullable типов (объяснение Nullable типов выходит за рамки статьи, но при желании вы можете почитать здесь). Например, функция isNullOrEmpty из стандартной библиотеки Kotlin, которая расширяет тип CharSequence?, может быть использована следующим образом:

Сигнатура этой функции представлена ниже:

При работе из Java с такими Kotlin функциями, extension функции доступны как статические.

Infix функции

Очередной способ подсладстить синтаксис — это infix функции. Проще говоря, благодаря этому инструменту мы получили возможность избавиться от лишнего зашумления кода в простых ситуациях.
Блок assertions из основного примера статьи демонстрирует использование этого инструмента:

Такая конструкция эквивалентна следующей:

Есть ситуации, когда скобки и точки излишни. Именно на этот случай нам нужен infix модификатор для функций.
В коде выше, конструкция teacherSchedule[day, lesson] возвращает элемент расписания, а функция shouldNotEqual проверяет, что элемент не равен null.

Чтобы объявить такую функцию необходимо:

Вы можете комбинировать два последних инструмента, как в коде ниже:

Обратите внимание, что дженерик тип по умолчанию наследник Any (не Nullable иерархии типов), однако, в таких случаях, мы не можем использовать null, по этому необходимо явно указать тип Any?

Контроль контекста

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

До версии Kotlin 1.1 уже существовал способ, как этого избежать. Создание собственного метода data во вложенном контексте DataContext, а затем пометка его аннотацией Deprecated с уровнем ERROR.

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

В Kotlin 1.1 появился новый инструмент для контроля — аннотация @DslMarker. Она применяется на ваши собственные аннотации, которые, в свою очередь, нужны для маркирования ваших контекстов. Создадим свою аннотацию, которую пометим с помощью нового инструмента в нашем арсенале:

Затем необходимо разметить контексты. В нашем основном примере это SchedulingContext и DataContext. Благодяря тому, что мы помечаем каждый из классов единым маркером DSL, происходит следующее:

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

В этом примере у нас появляется третий уровень вложенности и вместе с ним новый контекст Student, который, на деле, сущностной класс, часть модели, а значит нам нужно пометить аннотацией @MyCustomDslMarker еще и сущностную модель, что, на мой взгляд, не верно.

В контексте Student вызовы data <> всё так же запрещены, т.к. внешний DataContext никуда не делся, но эти конструкции остаются валидны:
javaschedule < data < student < student < >> > >

Пытаясь решить эту проблему с помощью аннотаций, у нас смешивается код для тестирования и бизнес код, а это в большинстве случаев нам не подойдет. Решения здесь три:

Использовать дополнительный контекст для создания студента, например, StudentContext. Это похоже на безумие и перестает оправдывать преимущества @DslMarker.

Создать интерфейсы для всех сущностей, например, IStudent (наименование здесь не важно), создать контексты-пустышки, наследующие эти интерфейсы, и делегировать реализацию объектам студентов, что тоже на грани бреда.
@MyCustomDslMarker
class StudentContext(val owner: Student = Student()): IStudent by owner

Воспользоваться аннотацией @Deprecated, как в примерах выше. В данном случае, пожалуй, это лучшее решение, которым можно воспользоваться.
Просто добавляем deprecated extension метод для всех Identifiable объектов.

В итоге, комбинируя разные инструменты, мы строим комфортный DSL для решения наших задач.

Минусы использования DSL

Попытаемся быть более объективными в применении DSL на Kotlin и разберемся, какие минусы есть у использования DSL в вашем проекте.

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

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

Возможно, вы подскажете интересные варианты, но сейчас мне известно два решения этой проблемы: добавлять «именованные callback’и», как составляющую DSL, или плодить лямбды. Второй вариант проще, но его последствия могут превратиться в самый настоящий ад, когда вы пытаетесь отследить последовательность вызовов. Естественно, когда у нас появляется много императивного поведения подход с DSL начинает от этого страдать, отсюда и эта проблема.

This, it!?

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

Наличие контекста может сбить с толку человека, который с ними никогда не работал. Однако теперь в вашем арсенале есть «лямбда с контекстом» и вас стало еще труднее поставить в тупик появлянием странных методов внутри DSL. Помните, что на крайний случай вы можете присвоить контекст переменной, например, val mainContext = this

Вложенность

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

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

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

Заключение

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

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

Потренируйтесь «на кошках», как герой одного известного фильма, сделайте DSL для тестов, а затем, сделав множество ошибок, и после появления опыта, рассмотрите и другие применения.
Желаю успехов в разработке проблемно-ориентированных языков!

Источник

Читайте также:  modern payment standa sankt peterb rus что это
Сказочный портал