Раздел 16 · Архитектура и решения

Слои как морфизмы: категорная модель гексагональной архитектуры

senior~60 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Слои как морфизмы: категорная модель гексагональной архитектуры

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

  1. Слой знает только интерфейс того, кто под ним, а не его устройство.
  2. Адаптер можно заменить, и всё, что выше, продолжит работать без правок.
  3. Слои складываются в стопку любой высоты, и неважно, как ты группируешь соседние пары.

Пока это обещания, их нельзя проверить. В этом уроке ты превратишь их в теоремы. Для этого понадобятся три вещи: категория контейнеров, где интерфейс становится объектом, утверждение «слой один к одному это естественное преобразование функторов» и свободная монада, которая даёт композицию слоёв даром. По дороге выяснится, почему ответ HTTP всегда идёт против стрелки, то есть контравариантен.

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

Минимум теории категорий

Определение 1. Категория состоит из объектов, множеств морфизмов для каждой пары объектов, тождественного морфизма и композиции для и . Требуются два закона: и .

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

Определение 2. Функтор сопоставляет каждому объекту объект , каждому морфизму морфизм и сохраняет тождество и композицию: , . Функтор это то, что в коде называют контейнерным типом с map.

Определение 3. Естественное преобразование между функторами это семейство морфизмов , по одному на объект, такое, что для любого коммутирует квадрат:

То есть . В коде это полиморфная функция <X>(fx: F<X>) => G<X>, которая не смотрит на элементы: ей всё равно, сначала переложить содержимое и потом применить map, или наоборот.

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

Интерфейс как контейнер

Порт в гексагональной архитектуре это набор команд, и на каждую команду есть свой тип ответа. Команде findByEmail отвечают «пользователь или ничего», команде insert отвечают «готово». Тип ответа зависит от команды. Ровно это и описывает контейнер.

Определение 4. Контейнер это множество форм и семейство множеств позиций , по одному множеству на каждую форму .

Определение 5. Расширение контейнера это функтор :

Элемент расширения это пара: форма и функция , которая каждой позиции формы сопоставляет значение из . Законы функтора очевидны: и .

Привычные типы данных оказываются контейнерами:

  • Maybe: формы , , . Функция из пустого множества одна, поэтому у nothing нет содержимого.
  • Список: формы это длины , позиции , функция это сами элементы.

Для архитектуры нужно другое прочтение. Пусть формы это команды, а позиции это возможные ответы на команду. Тогда элемент это один шаг взаимодействия: «отправь команду ; когда придёт ответ , продолжай как ». Такое прочтение интерфейсов идёт от Хэнкока и Сетцера.

Порты нашего примера:

У позиции постоянны: на любой запрос отвечают значением одного типа . Тогда расширение упрощается до . Запомни, где тут стоит : слева от стрелки функции. К этому месту мы вернёмся.

Морфизм контейнеров и категория Cont

Как выглядит слой, который каждую свою команду обслуживает ровно одной командой нижнего порта? Он должен перевести команду вниз, а когда придёт ответ снизу, перевести его обратно наверх. Направления у двух переводов противоположные.

Определение 6. Морфизм контейнеров это функция на формах и семейство функций на позициях для каждой .

Формы идут вперёд, позиции идут назад. Слой отправляет вниз команду , получает ответ из и превращает его в ответ на исходную команду.

Определение 7. Композиция морфизмов и :

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

Предложение 1. Контейнеры с их морфизмами образуют категорию с тождеством .

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

Если вычеркнуть зависимость, то есть взять постоянные позиции, морфизм становится парой функций и . Это ровно линза: get вперёд и put назад, причём put снова видит исходный запрос. Поэтому морфизмы контейнеров называют зависимыми линзами.

Слой это морфизм функторов

Морфизм контейнеров переводит шаги взаимодействия: шаг верхнего интерфейса становится шагом нижнего. На расширениях он задаёт семейство функций

Типы сходятся: и , значит . Продолжение ждало ответ верхнего уровня; теперь оно сначала переводит нижний ответ через . Естественность: , то есть квадрат из определения 3 коммутирует.

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

Теорема 1 (Abbott, Altenkirch, Ghani). Сопоставление , это полностью точный функтор . Иначе говоря, каждое естественное преобразование имеет вид ровно для одного морфизма .

Доказательство. Сначала функториальность. Тождество переходит в . Для композиции посчитай , а это по определению 7.

Теперь биекция. Обозначим , представимый функтор. Определение 5 говорит, что расширение это сумма представимых: . Для любого функтора получаем цепочку биекций:

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

Подставь :

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

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

Отсюда первая точная формулировка: слой, который обслуживает каждую команду одним вызовом вниз, это естественное преобразование между функторами интерфейсов, и ничего, кроме морфизма контейнеров, он содержать не может. Естественность здесь значит в точности обещание 1 из начала урока: слой не смотрит в продолжение , то есть не знает, что верхний код сделает с ответом. А стрелка смотрит туда же, куда стрелка зависимости на картинке гексагона: сверху вниз.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.