Почти месяц назад я с коллегой начал создавать новую библиотеку под названием BitWiser. Это библиотека, которая помогает разработчику работать с битами, байтами и полубайтами. Много работая с bluetooth, я обнаружил, что было бы интересно написать DSL близко к SwiftUI, но для создания объектов данных. В этой статье я объясню, как создать свой собственный DSL, как это сделал я в BitWiser.

МАГИЯ

Вероятно, вы уже видели такой фрагмент кода и знаете, откуда он взялся.

И, вероятно, как и я, вы видели немного волшебства в SwiftUI.

Как это возможно? каждая строка кажется инструкцией, они представляют собой экземпляры представлений, и в них нет переменных.
Это похоже на массив, но вы можете написать if операторы, а объекты не разделяются запятыми.
Этого можно достичь с помощью используя построители результатов (ранее известные как построитель функций).

Я ДО СИХ ПОР НЕ ПОНИМАЮ, КАК ЭТО РАБОТАЕТ

Вот что происходит при использовании ViewBuilder, сердце SwiftUI:

Эти строки кода интерпретируются в это во время компиляции:

Я предполагаю, что вся магия потеряна, но это именно то, что происходит под капотом.

ПОЧЕМУ Я ДОЛЖЕН ИХ ИСПОЛЬЗОВАТЬ?

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

АНАТОМИЯ КОНСТРУКТОРА РЕЗУЛЬТАТОВ

Построитель результатов построен на трех типах (фактически типах с псевдонимами):
- Компонент: базовый строительный блок
- Выражение: когда вы хотите заставить их работать с разными типами ввода
 – Конечный результат: когда вы хотите, чтобы строители работали с другим типом в качестве конечного результата.

Чтобы создать построитель результатов, вам понадобится хотя бы этот оператор и аннотация @resultBuilder.

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

ДАВАЙТЕ СОЗДАЕМ КОНСТРУКТОР MARKDOWN(-ish)

Конструктор документов должен поддерживать следующие теги:
— Заголовок первого уровня (#)
— Заголовок второго уровня (##)
— Заголовок третьего уровня (###)
— Основная часть

Вывод будет показан как обычный String с тегами уценки.
В основном нам нужно:
- интерфейс, который может преобразовать общую строку в одну с правильным тегом уценки: MarkdownConvertible
- ряд объекты, которые могут быть инициализированы общей строкой, для которой мы должны реализовать описанный выше интерфейс, чтобы создать тегированную версию самой строки. У нас будет: LevelOneHeader, LevelTwoHeader, LevelThreeHeader, Body

- объект, который представляет документ уценки и который можно инициализировать, используя все вышеперечисленные части с помощью построителя результатов, MarkdownDoc

@MarkdownCreator — это наш генератор результатов, давайте посмотрим, как он работает.

Как я уже писал ранее, для создания построителя результатов нам как минимум нужна аннотация @resultBuilder и метод. Первая реализация будет выглядеть так:

Мы берем компоненты var-arg, перебираем их и составляем строку, которая является результатом преобразования MarkdownConvertible.

Это позволит нам написать такой код:

Это круто, но очень просто, мы бы предпочли написать что-то более сложное и гибкое.

Сначала давайте начнем разрешать использование оператора if, для этого мы должны добавить этот метод:

Этот метод включает только поддержку оператора if без else. Для if-else должны быть реализованы два других метода:

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

Вот как на самом деле выглядит if-elseimplementation.

Циклу forнужен, угадайте что, еще один метод:

Реализация очень похожа на реализацию с var-args и используется для объединения всех строк.

Теперь мы можем создавать документ в различных ситуациях выразимым образом, мы можем использовать if-else операторов, switch, а также for циклов.
Мы создали DSL, который упростил бы создание документа с уценкой (-ish) для всех.

ПОСЛЕДНИЕ МЫСЛИ

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

Я использовал конструктор результатов для создания библиотеки под названием BitWiser. Я также добавил DSL для создания объекта данных из любого объекта, соответствующего определенному протоколу. Вы можете делать потрясающие вещи с конструктором результатов, и это только верхушка айсберга.

Вот ссылка на коллекцию репозиториев, которые использовали их для создания красивых вещей: конструктор потрясающих результатов.