Комментарии 9
Давайте упростим. А то тяжело читать, хоть я и математик по образованию.
Естественные преобразования - короткая версия
Зачем это нужно?л
В программировании мы работаем с разными "контейнерами" типов: List, Option, Future и т.д. Часто нужно переходить от одного к другому, например List[A] => Option[A].
Проблема: Не все такие преобразования одинаково хороши. Некоторые ведут себя непредсказуемо при композиции с другими функциями.
Что такое естественное преобразование?
Это универсальная функция F[X] => G[X], работающая для любого типа X, которая коммутирует с функторами.
Условие естественности (простыми словами):
val f: A => B = ???val α: F ~> G = ??? // наше преобразование// ДВА ПУТИ должны давать ОДИНАКОВЫЙ результат:// Путь 1: сначала преобразовать F[A] => G[A], потом применить f// Путь 2: сначала применить f к F[A] => F[B], потом преобразовать F[B] => G[B]α[B](fa.map(f)) == α[A](fa).map(f) // должно быть TRUE всегда!Пример естественного преобразования:
val headOption: List ~> Option = [A] => (list: List[A]) => list.headOptionДве композиции
Вертикальная (обычная последовательность):
val α: F ~> Gval β: G ~> Hval result: F ~> H = β ⋅ α // сначала α, потом βГоризонтальная (когда функторы сами композируются):
val α: F1 ~> F2val β: G1 ~> G2val result: (G1 ∘ F1) ~> (G2 ∘ F2) = β ∘ αЕстественный изоморфизм
Два взаимообратных естественных преобразования:
val туда: F ~> Gval обратно: G ~> F// Должно выполняться:(обратно ⋅ туда)(fa) == fa(туда ⋅ обратно)(ga) == gaЭто означает, что F и G по сути "одинаковые", просто представлены по-разному.
Естественные преобразования - это "правильные" способы переходить между функторами, которые:
Работают предсказуемо
Композируются хорошо
Не зависят от порядка применения функций
Они нужны для построения монад, линз и других конструкций функционального программирования.
Практические применения естественных преобразований
1. Конвертация между контейнерами
List[A] => Option[A] // headOption
Option[A] => List[A] // toList
Try[A] => Either[E, A] // toEither
Future[A] => Task[A] // конвертация эффектов2. Работа с эффектами
// Запуск отложенных вычислений
IO[A] => Future[A] // unsafeToFuture
Task[A] => Observable[A] // toObservable3. Упрощение вложенных структур
Option[Option[A]] => Option[A] // flatten
List[List[A]] => List[A] // flatten4. Интеграция библиотек
Cats → ZIO
Akka Streams → FS2
ScalaZ → Cats
Java Optional → Scala Option
5. Тестирование
// Замена реальных эффектов на тестовые
Real[A] => Mock[A]
IO[A] => Id[A] // синхронное выполнение для тестов6. Оптимизация
Stream[A] => Vector[A] // материализация
LazyList[A] => List[A] // строгое вычисление7. Обработка ошибок
Either[E, A] => Validated[E, A]
Try[A] => IO[A]Зачем это программисту?
✅ Безопасность - гарантия, что преобразование не сломает логику
✅ Композируемость - можно свободно менять порядок операций
✅ Переиспользование - один раз написал, работает для всех типов
✅ Рефакторинг - можно менять контейнеры без изменения бизнес-логики
Композируемость - это скорее про саму возможность собирать из простого сложное, а не про изменение порядка операций.
А в целом всё верно. Краткий конспект на две минуты для десятиминутной статьи.)) Наверняка кому-то будет полезен, спасибо!
Я строго убеждён, что для программиста ответ на вопрос
Зачем это нужно?
очень простой: это не нужно (как и весь остальной теоркат).
А вот для математика (и интересующегося программиста, но он в этот момент тоже такой маленький зачаток математика) это просто ещё одна ступень обобщений, когда нужно больше морфизмов. Собсна:
Что такое категория? Это когда есть объекты и морфизмы между ними с такими-то законами.
Что, если сказать, что категория — это тоже объект? Как тогда будут выглядеть морфизмы между ними? Ура, мы получили функторы — это морфизмы между категориями (которые, кстати, образуют категорию Cat или не-во-всех-основаниях-категорию CAT, в зависимости от требований малости категорий).
Что, если сказать, что функтор [между фиксированными категориями 𝒜 и ℬ] — это тоже объект? Как тогда будут выглядеть морфизмы между ними? Ура, мы получили естественные преобразования — это морфизмы между функторами (которые, кстати, образуют категорию [𝒜; ℬ]).
Что, если сказать, что естественное преобразование — это тоже объект? Как тогда будут выглядеть морфизмы между ними? Ура, мы получили модификации. Я не знаком с общепринятым обозначением их категорий.
Что, если перестать тупо строить лестницу в небо и сразу построить замыкание всего этого процесса? Ура, мы получили n-категории, (∞,n)-категории, и прочую подобную хтонь.
✅ Безопасность - гарантия, что преобразование не сломает логику
✅ Композируемость - можно свободно менять порядок операций
✅ Переиспользование - один раз написал, работает для всех типов
✅ Рефакторинг - можно менять контейнеры без изменения бизнес-логики
А вот это всё — это, как говорится, wishful thinking, и в каждом конкретном случае прибегать к аппарату теорката не нужно.
Супер
В таких же словах объясните монаду, пожалуйста. Тоже хтонь, но из программирования.
Ранее уже делился своими соображениями по поводу вопроса Программисту нужна математика?
А вот это всё — это, как говорится, wishful thinking
Уж не знаю, где так говорят... Но похоже, что мне не удалось донести очень важную идею. Перечисленные товарищем @Dhwtjпункты - это вовсе не попытки "выдать желаемое за действительное". Это именно мотивация для того, чтобы захотеть все эти абстракции, для которых математики уже просто придумали названия. Возможно, в это сложно поверить, но именно по этим причинам все эти абстракции появились в программировании, стали известны программистам.
Что, если сказать?..
Вот как раз программистам вовсе не нужно задавать такие вопросы. Наоборот, сначала (в том числе и в программировании!) возникает необходимость, для которой формулируются задачи, решениями которых становятся все эти абстракции. То, что они были известны математикам раньше, не имеет большого значения - математики и так знают много того, что программистам особо не нужно.
Дело не в том, что программистам "нужны все эти морфизмы". Решение задач программирования требует инструментов, для которых должны выполняться определённые законы. Как раз законы этих инструментов и определяют абстракции, для которых у математиков уже есть свои названия. Именно это я пытаюсь раскрыть в своём обзоре.
Если докопаться до основ ФП, то там стройная но сложная математика.
Если докопаться до основ ООП, то там треш, угар и содомия. Но на поверхности всё хорошо и удобно: кошечки, собачки. А при параллельных запросах треш. Композиция треш. Доказательства корректности треш.
Alan Kay - message passing, изоляция. Слишком медленно! shared mutable state + блокировки
То, что они были известны математикам раньше, не имеет большого значения
Это форсайт, предвидение
Ранее уже делился своими соображениями по поводу вопроса Программисту нужна математика?
Да, я видел эту статью и даже там что-то комментировал. Правда, не помню, что именно, но, на всякий случай повторю свой основной тезис: программисту математика не нужна. Конкретнее, этак 99% программистов может решить этак 99% задач, используя школьные знания математики, и ещё 0.9% задач, используя здравый смысл и первые несколько абзацев в соответствующих статьях на википедии.
Кстати, самое смешное, что упомянутая выше lstToOption не является естественным преобразованием (если List в этой вашей скале строгий), потому что для е.п. требуется, чтобы ∀ f : A → B. lstToOption_B ∘ List f = Option f ∘ lstToOption_A, а это, очевидно, не так для f ≔ x ↦ 1 / x: свидетелем неравенства будет список [1, 0], например.
Возможно, в это сложно поверить, но именно по этим причинам все эти абстракции появились в программировании, стали известны программистам.
А они стали известны программистам? В это правда сложно поверить. Сколько программистов хотя бы знает про (я уж не говорю о «интернализировало, построило удобную ментальную модель и комфортно работает с») те же естественные преобразования?
Сколько программистов глядит на maybeToList :: Maybe a → [a] и думает «о, естественное преобразование!»? Сколько людей пишет обычный продуктовый код вроде
template<typename T>
std::optional<T> safeIndex(const std::vector<T>& vec, size_t idx)
{
if (idx >= vec.size())
return {};
return vec[idx];
}и вспоминает про е.п.?
Я работал в разных компаниях, от мелких российских стартапов по машинному обучению при физтеховской кафедре, собсна, машинного обучения, до всяких американских корпов, крупнейших в своей области, и мой ответ: ноль.
Ну, почти ноль. Не нулём это было ровно в одной компании, где, так оказалось, собрались большие энтузиасты теорката и писали там всякие компиляторы, тайпчекеры и прочее для одного предметного языка. Но это телега впереди лошади: эти люди сначала изучили теоркат из личного интереса, а потом оказалось, что теоркат не очень кормит, и надо бы писать код, а компиляторы писать всё же интереснее, чем опердни.
Поэтому да, эти абстракции программистам неизвестны, и, более того, опять же не нужны (то есть, без знания этих абстракций можно успешно решать продуктовые задачи).
Вот как раз программистам вовсе не нужно задавать такие вопросы. Наоборот, сначала (в том числе и в программировании!) возникает необходимость, для которой формулируются задачи, решениями которых становятся все эти абстракции.
Как же незнакомые с этими абстракциями люди решают задачи? (Ответ: вполне успешно решают, успешнее всех этих скалистов и хаскелистов, и если смотреть на вакансии и зарплаты, то оказывается выгоднее разбираться в кэшлайнах и ROB и знать, что начиная с этак Alder Lake ренеймер регистров умеет прибавлять небольшие значения на этапе ренейминга, сводя стоимость add с фиксированной константой к нулю циклов, ну и софт-скиллы там какие-то, чем уметь доказать лемму Йонеды или показать, как именно строятся пределы в функтор-категориях).
Мета-ответ (и мой мета-поинт): нет ничего зазорного в том, чтобы зарабатывать на хлеб унылым с теоретической точки зрения программированием, а математику оставить на вечера и выходные из любви к искусству. Более того, по моему личному опыту это единственно возможное долгосрочно стабильное деление.
Нет ожиданий — нет разочарований.
Нет ожиданий — нет разочарований.
Я понимаю такую позицию, но не разделяю её. Страх перед разочарованиями отнимает мечты и стремление к лучшему.
Кстати, самое смешное, что упомянутая выше
lstToOptionне является естественным преобразованием.
Вполне себе является. Естественные преобразования определяются только в контексте чистых функций, а f ≔ x ↦ 1 / x не является таковой на значениях целочисленного типа.
А они стали известны программистам?
Безусловно. Я же говорил не обо всех программистах, а о тех, кто с ними работает, например, в стеке CE или ZIO в Scala. Сам я познакомился с ними ещё будучи C#-разработчиком - уже тогда искал и находил более выразительные ФП-шные способы решения задач. И уж тем более эти абстракции известны разработчикам языков, которые лучше других понимают, какие инструменты будут полезны программистам.
Сколько людей пишет обычный продуктовый код...
...нет ничего зазорного в том, чтобы зарабатывать на хлеб унылым с теоретической точки зрения программированием...
Как же незнакомые с этими абстракциями люди решают задачи?
Да хреново они решают!)) Это вовсе не попыктка кого-то обидеть, это очевидный факт. Я на собственном опыте оценил качественный скачёк при переходе на ФП-шное мышление. Типичная кодовая база на популярных ЯП (в которых обычно никто не ожидает понимания ФП с его математическим базисом) - это тонны слаботипизированной копипасты, которую крайне тяжело читать и рефакторить. Тестирование просто не способно закрыть все дыры и явные ошибки, которыми полнится такой код. И это притом, что даже в популярных ЯП давно уже завезли кучу всякой функциональщины. С тех же плюсов я слез уже много лет назад, но помниться и тогда много вкусностей было во всяких stl/boost. Но очень много программистов просто не знакомо с этим - у них нет какой-либо математической базы чтобы даже захотеть поискать лучшие решения! В основной массе все говнокодят постаринке, и да, им платят за это деньги. Но такие проекты живут 3-5 лет, потому что потом становится слишком затратно/больно месить этот говнокод.
У нас есть выбор с каким кодом работать. Я - за качественный.

Категории типов. Часть 3. Естественные преобразования