Привет Хабр! Пятничного тру ФП хардкора с Free Monad, Таглес Финал, Монад трансформерами, Refined Types, Smart Constructors и прочим таким вам в ленту. Хардкор сам себя в ленту не принесет так что погнали.  Гексагональная архитектура делит наш код на три основные части.  1) Primary Adapters, 2)  Secondary Adapter  3)  Logic aka Domain.  Primary Adapters изолируют наш домен от вызовов снаружи, фремворков и библиотек вызывающих наш код и эффектов вызванных ими. Например, ввод пользователя. Secondary Adapters изолирует нас и дает абстракции для работы из нашего кода с внешним миром, библиотеками и фреймворками которые вызывает наш код. Например, запись данных в файл. Так для DDD очень хорошо реализуется через Free Monad и Refined Types за счет написания DSL для домена и под доменов, а так же создания Value Objects через Refined Types. Далее мы будем реализовать Secondary Adapters через Free Monad и на созданном нами DSL уже писать код внутри Primary Adapters отдавая его наружу где он уже будет компилироваться.  Вся суть – в Domain описываем наш DSL на Free Monad и пишем на нем код. Primary Adapters отдают этот код наружу. Secondary Adapters содержит компиляторы для нашего кода. Содержит сущности, присутствующие в нашем домене и являющиеся структурами данных. У сущностей может быть своя собственная логика. В ООП это делается с помощью методов сущностей. Тут мы будем делать это с помощью тайпклассов связанных с нашими сущностями. Например, Movable[A] и Movable[User] поместим в это слой.  Value Objects – объекты что сравниваются по всем их полям и являются типом для каких то базовых данных в домене. Это кирпичики, атомы из которых уже строиться все остальное. Например, UserName, HumanAge, Money, Inn (ИНН). Обертка над значением что дает ему свой собственный тип. Хорошо реализуются с помошью Refined Types   Entities – Сущности что сравниваются только по их уникальному идентификатору (id). Например, User, Order. Тут могут быть полезны проверки валидности типа с помошью паттерана Smart Constructors например чтобы проверить что какие-то два поля сушности равны друг другу если для сущностей данного типа предпологается что эти поля всегда должны быть равны или например для сущност представляющей собой тип для периода что его начало всега раньше его конца. Period.Start < Period.End. Aggregate – набор сущностей собранных вместе для удобной работы с ними. Чтобы управлять ими как чем-то единым. Например, Order и OrderItem.  В ФП это работает через тайп классы для AggregateRoot (Если представить связь сущностей как граф в виде дерева, то это будет его корень). Order это AggragateRoot а OrderItem и OrderAddress это входящие в него объекты. Так, чтобы понять чуть подробней цель создания Aggregate давать посмотрим пример создания объекта Car для компьютерной игры с раздельными повреждениями. У нас есть корень Агрегата — это Машина. У нее есть части — это Двигатель, Колеса, Бензобак. У каждого из них есть свой запас прочности. Теперь нам надо написать код для уменьшения их прочности при столкновении. Мы можем достать из хранилища (БД той же) отдельно каждое колесо, уменьшит его прочности. Сохранить его в БД. Потом достать двигатель и уменьшить его поле HP и т.д. А можем сразу достать весь Aggregate и вызвать у него метод или вызвать метод typeclass на Aggregate в котором разом сразу уменьшится HP всех его частей. Потом разом одним запросом мы можем сохранить все его части в БД. getCar, saveCar, setHp(car, hp). Вместо getEngine, saveEngine, setHp(engine, hp). getWheel, saveWheel, setHp(wheel, hp) и т.д. Там же позицию в пространстве можно разом изменить одним методом для всей машины а не изменять координаты каждой из ее частей. Movable[A] и Movable[Car]. Сервис — это объект, не являющийся структурой данных. Для ФП тут корректнее рассматривать сервис как набор функций. В общем тут скорее Domain. Functions более корректное название. Тут в ООП обычно лежат интерфейсы (абстракции ака trait или interface) репозиториев и других объектов для взаимодействия с внешним миром. Так же тут сервисы с логикой доменной. В ООП обычно все в методах самих Entities делается и тут только всякие Validators, Calculators лежат иногда. Тут же мы будем описывать наш DSL на Free. Компилятор же будет в слое SecondaryAdapters.    Задача этого слоя изолировать нас и абстагироваться от того кто из внешнего мира который вызывает наш код. От контроллеров, хендлеров и прочего. От фреймворков внешних и библиотек вроде PlayFramework, Http4s.Это делает для того чтобы было легко перенести наш код с одного фреймворка на другой. Есди мы захотим чтобы наш код начал вызвать другой фреймворк. Например на Akka-Http. Тут мы создаем код программы на нашем DSL и отдаем его наружу. Скомпилирован он будет уже в самом приложении в Controllers или просто в Main если у нас консольное приложение. Например, AuthService содержит набор функций что возвращает программы для работы с авторизацией и аутентификацией пользователя. Проверка валидности пароля, выдача токена и прочее. Этот слой исполняет роль Anti Corruption Layer  Задача этого слоя абстрагироваться и изолировать нас от внешнего мира и библиотек или фреймворков что мы вызываем чтобы легко было их заменить. Например библиотеку для работы с БД doobie на какую нибудь другую. Так же тут стартовая точка для "неопределенности", "хаоса", эффектов. Именно этот слой мокается в тестах или для него делают тестовые реализации через монаду Id. Тут обычно лежат реализации репозиториев в ООП. Абстракции над источниками данных. Чистый код – код без протекающих абстракций. Здесь реализации абстракций что работают с библиотеками и фреймворками для ввода вывода или возвращают рандомные значения. Для ФП тут мы будем компиляторы для DSL описанного в домене реализовать. Тут работы уже идет с IO, библиотеками для доступа к БД, Запросам по сети, Доступа к файлам конфигураций. Вообще все что создает эффекты и возвращает значения, не зависящие от входящих параметров функции в том числе случайные значения. Например, UUID.randomUUID() вызывать допустимо только в этом слое потому что этот метод возвращает разные значения при вызове с одним и тем же набором параметров (без параметров точнее). В общем посмотрите на функцию. Если она может вернуть разные значения при вызове с одним и тем же списком параметров значит ее место здесь. В этом слое изолируется неопределённость, хаос. Например чтение данных пользователя из БД, файла или из API какого-то сервиса может быть реализовано через функцию getUserById и если три раза вызвать этот метод getUserById(“1”) с перерывом в несколько дней то он может вернуть совершенно разные значения (пользователя удалили, пользователя перименовали) хотя мы передаем все тот же самый параметр id = “1”. Так же к этому относятся Instant.now() и Random.nextInt().Этот слой тоже исполняет роль Anti Corruption Layer. Тут же весь наш SQL код находиться. Наше приложение и специфичные для него вещи. Файлы миграций и конфигураций. Контроллеры и хендлеры. Точнее специфинчные для основной библиотеки нашего приложения от которой мы изолируемся. У меня сейчас это Tapir + Http4s поэтому тут будет весь код связанный с ними. Тот же результата можно достичь без Free с обычными тайпклассами пример Исодники примера (основной код в папке core) CIO