Для решения проблем из предыдущего поста я пробовал разные подходы в течение нескольких лет, но в начале прошлого года мои идеи окончательно кристаллизовались и попали в
tofu.
Итак, решение было таким:
1. Ввести абстрактный DB-эффект.
2. Создать фасад над транзактором, поддерживающий работу с произвольным DB-эффектом.
3. Ввести тайпклассы для лифтинга в DB-эффект операций с БД (описываемых в
ConnectionIO) и дополнительных нетранзакционных действий (описываемых в
F).
4. Использовать выше обозначенные сущности для описания транзакционной логики в TF-стиле.
5. Выбрать достаточно мощную монаду — замену для
ConnectionIO, для которой можно было бы вывести все нужные инстансы и реализовать соответствующие лифтинги и транзактор с поддержкой типизированного контекста.
6. Научить
LogHandler работать с контекстным логером.
С пп. 1–4 было всё понятно, сложность представляли остальные два пункта.
На выбор новой DB-монады меня подтолкнуло то, что
ConnectionIO в CE2 имела инстанс
LiftIO, а сама
IO тогда была неким универсальным способом интеропа между любыми библиотеками эффектов. Казалось, что
ConnectionRIO[R, *] (обычный
ReaderT поверх
ConnectionIO) решает все проблемы: контекстные эффекты легко лифтятся в неё через ряд преобразований, написать аналог
Transactor, умеющий интерпретировать
ConnectionRIO в такой же контекстный эффект, тоже не очень сложно. Для
LogHandler была написана специальная обёртка, позволяющая через
UnliftIO и
Embed встроить сайд-эффектящий, но не теряющий контекст хэндлер в любой DB-модуль.
С этим решением в прод поехали несколько моих сервисов и проекты других компаний, где используют
tofu.
Сюрприз прилетел весной этого года, когда я начал наконец погружаться в CE3 и соответствующие изменения в
doobie. Оказалось, что из-за общего редизайна залифтить в
ConnectionIO стало возможно только
Future — а получить её в чистом виде, вне контекста
F, достаточно проблематично. Пришлось искать другой способ выражения DB.
Даже не помню, как я к этому пришёл, но решил рассмотреть континуальное представление
ConnectionIO как альтернативу
ConnectionRIO, что-то типа:
type DB[F[_]] = [x] =>> ConnectionIO ~> F => F[x]
Этого оказалось достаточно, чтобы залифтить не только
ConnectionIO, но и любой произвольный эффект
F без промежуточных конверсий и явного извлечения контекста. Более того, такая форма позволяет почти бесплатно выводить большинство инстансов для
DB через инстансы для
F. Можно выкинуть огромную простыню дериваций лифтингов, которые я когда-то написал для
ConnectionRIO.
Больше всего я опасался невозможности выразить транзактор с сохранением API и поддержкой стримов. Но к счастью,
doobie.Transactor задизайнен достаточно хорошо, чтобы это стало осуществимым — интерпретация и транзакционная стратегия легко отделимы друг от друга.
Всё получилось даже на CE2. Жаль, я не додумался до континуального представления раньше. Спасибо дедушке Йонеде за нашу прекрасную молодость.
Отправил сегодня
PR в
tofu. Там есть небольшой пример использования (вместо доки), он почти не изменился — наоборот, кое в чём стал даже проще. В целом этот подход я воспринимаю как гораздо более правильный и чистый. Надеюсь, в последний момент не вскроется что-нибудь критичное. Что касается
LogHandler — работа с ним стала чуть проще, за счет инстанса
UnliftIO[DB], но когда мигрируем
tofu на CE3 — окончательно с ним разберусь.
Кстати, по миграции на CE3 стало заметно, как Rob Norris забросил развитие
doobie и посвятил все силы
skunk'у. Последний я пока не распробовал — смущает собственная имплементация Postgres-протокола. Впрочем, слышал от уважаемых юзеров хорошие отзывы. Надо будет затащить куда-нибудь и сравнить их эргономику. Как минимум, трейсинг в
skunk уже вшит, это обнадеживает.