Поясняю за Mid
по запросу @aleksei_t
Исходная мысль была такая. Итак у нас есть
trait MyBusinessModule[F[_]]{
def doBusinessThing(entity: Entity, info: Info): F[Value]
def undoBusinessThing(entity: Entity): F[Respect]
}
Мы привыкли что F - это какой-то там IO
- или какой-то трансформер, ридер - шмидер.
Однако ничто в сигнатуре не обязывает нас к такой строгости, мало того, ничего не обязывает нас использовать что-то, что вообще функтор там.
Ну давайте с простого
type Pre[F[_], A] = F[Unit]
это такой странный тип, который вроде и имеет тайп-параметр, но ему всё равно, никакой информации о результате он не несёт
применив MyBusinessModule
к Pre[F[_], *]
мы получим такой тип
//MyBusinessModule[Pre[F, *]]
{
def doBusinessThing(entity: Entity, info: Info): F[Unit]
def undoBusinessThing(entity: Entity): F[Unit]
}
результата мы не производим - только какой-то "эффект", как принято говорить, на основе входных параметров, так можно выразить логирование, проверку входных данных или что-то вроде такого
что, если дальше мы напишем
type Post[F[_], A] = A => F[Unit]
получили и вовсе контравариантный тип по отношению к A
. Но что с нашим модулем?
//MyBusinessModule[Post[F, *]]
{
def doBusinessThing(entity: Entity, info: Info): Value => F[Unit]
def undoBusinessThing(entity: Entity): Respect => F[Unit]
}
Получили новую сущность, когда результаты уже получены мы можем взять одну или несколько таких штук, чтобы логировать уже результаты, отослать какие-то события о совершённых действия в кафку или бизнес-лог.
И довершает всё следующий тип
type Mid[F[_], A] = F[A] => F[A]
При наличии Monad[F]
мы можем превратить и Pre
и Post
в такой Mid
Наш модуль, будучи применённым к нему выглядит так
//MyBusinessModule[Mid[F, *]]
{
def doBusinessThing(entity: Entity, info: Info): F[Value] => F[Value]
def undoBusinessThing(entity: Entity): F[Respect] => F[Respect]
}
Такой компонент может всё то же, что и предыдущие, но так же может "запустить" F несколько раз, или вообще не запускать.
Т.е. в качестве таких middleware могут выступать кеширование, ретраи и вся остальная техническая или логическая суета, которая не реализуется в вашей инфраструктуре, или требует дополнительного осмысления или специфичного конфига.
Но как теперь использовать такие плагины?
Оказывается, достаточно ApplyK имея
def map2K[F[_], G[_], H[_]](af: A[F], ag: A[G])(f: Tuple2K[F, G, *]~> H]): A[H]
мы как раз и получаем, что если мы можем скомпозить пару "результат основного действия - результат плагина" - мы можем скомпозить и две реализации модуля - основную и подключаемую.
Ну т.е. вызываем этот map2K
подставляя F = F, G = Mid[F, *], H = F
отдаём наш MyBusinessModule[F]
и плагин MyBusinessModule[Mid]
в качестве af
и ag
, осталось только реализовать Tuple2K[F, Mid[F, *], *]~> F
Ну т.е. это примерно реализовать полиморфную функцию [A] (F[A], F[A] => F[A]) => F[A]
, реализация очевидно (fa, f) => f(fa)
Т.е. "аппликация" нашего плагина - это просто применение функции, но в результате каждого из методов.
Остальное - сделает макрос, сгенерировавший для вас ApplyK[MyBusinessModule]
Всё это раскидано где-то в исходниках этого модуля, который в принципе был сделан как базовый для core, чтобы макросом можно было выводить RepresentableK
для всех остальных штук.
Пример за авторством https://t.me/ppressives