Теория категорий

Функторы: map как математическая структура

Функтор - это map. `list.map(f)` в Python, `Array.map` в JavaScript, `torch.vmap` в PyTorch - все одно и то же абстрактное понятие. Теория категорий - это математика, которая называет это правильно. А в 2022 году Cruttwell et al. доказали: backpropagation - тоже функтор. 50 лет алгоритм использовали, не зная его формального определения.

  • **PyTorch vmap / JAX vmap**: `torch.vmap(f)` - это буквально функтор, поднимающий f: Tensor -> Tensor до f: Batch<Tensor> -> Batch<Tensor>. JAX построен на функциональных трансформациях (vmap, grad, jit) именно потому, что их функторная структура совместима с автодифференцированием
  • **Haskell / Scala / Rust**: Functor, Monad, Applicative - не паттерны GoF, а точные кодировки категорных конструкций. Компилятор GHC применяет fusion-оптимизацию на основе законов функтора: две цепочки .map() объединяются в один проход без intermediate списков
  • **TypeScript: вариантность типов**: параметры функций контравариантны, возвращаемые типы - ковариантны. Это не дизайн-решение TypeScript - это математическая необходимость, следующая из определения функтора
  • **Backprop as Functor (Cruttwell 2022)**: обратное распространение ошибки - функтор между категорией параметризованных функций и категорией их производных. Один из самых неожиданных результатов прикладной теории категорий

Что такое функтор

В 2022 году вышла статья «Backprop as Functor» (Cruttwell et al.). Авторы показали: обратное распространение ошибки - это функтор между категорией параметризованных функций и категорией их производных. 50 лет backprop использовали, не зная формального определения. Функторы оказались везде - просто раньше не называли их так.

Функтор F: C -> D - это отображение между двумя категориями, которое переносит и объекты, и морфизмы. Переносит без потерь: структура категории C должна отражаться в D точно. Не «примерно», не «по смыслу» - математически точно, через два закона.

Функтор F: C -> D - это пара отображений: 1. На объектах: каждому A в C ставит в соответствие F(A) в D 2. На морфизмах: каждому f: A -> B ставит в соответствие F(f): F(A) -> F(B) При этом оба закона обязаны выполняться: F(id_A) = id_{F(A)} (сохранение identity) F(g o f) = F(g) o F(f) (сохранение композиции)

Два закона - не просто формальность. Сохранение identity гарантирует: «пустое» действие остаётся пустым. Сохранение композиции гарантирует: два шага в C можно сделать через F раздельно или вместе - результат тот же. Это и есть «без потерь».

От Могги к Haskell

В 1989 году Эудженио Могги предложил использовать монады (особый вид функторов) для описания побочных эффектов в функциональных языках. Идея казалась чистой теорией. Филипп Уодлер превратил её в архитектуру Haskell. Сегодня Functor - один из базовых type class в функциональном программировании, а IO monad из той же статьи 1989 года управляет каждой строкой Haskell-кода в продакшне.

Функтор F: C -> D должен удовлетворять двум законам. Какие?

Ковариантный функтор: map в программировании

Функтор, который сохраняет направление стрелок - называется ковариантным. Если f: A -> B, то F(f): F(A) -> F(B). Стрелка идёт в том же направлении. Это и есть «обычный» функтор. В программировании его знают как `.map()`.

`Array.map`, `Promise.then`, `Option.map`, `torch.vmap`, `jax.vmap` - все ковариантные функторы. PyTorch `vmap` - это буквально функтор: он берёт функцию f: Tensor -> Tensor и поднимает её до f: Batch<Tensor> -> Batch<Tensor>, сохраняя все свойства. JAX построен на функциональных трансформациях именно потому, что функторная структура совместима с автодифференцированием.

ФункторКатегорияmap / fmap
**Array<T>**List functor[1,2,3].map(f)
**Promise<T>**Async functorpromise.then(f)
**Option<T>** / MaybeNullable functoropt.map(f)
**torch.vmap(f)**Batch functorf: Tensor -> Tensor поднимается до батча
**jax.vmap(f)**Batch functorvectorized map с XLA-оптимизацией

Проверка законов для Array. Закон identity: `[1,2,3].map(x => x)` даёт `[1,2,3]` - без изменений. Закон композиции: `arr.map(f).map(g)` равно `arr.map(x => g(f(x)))`. Это не просто «удобно» - это и есть определение корректного функтора. Если `.map()` с побочными эффектами нарушает закон - это не функтор.

Какое утверждение верно для ковариантного функтора Array?

Контравариантный функтор: стрелка разворачивается

Есть тип, который не производит значения T - а потребляет их. Предикат `(t: T) => boolean` принимает T. Компаратор `(a: T, b: T) => number` принимает два T. Сериализатор `(t: T) => string` принимает T. Для таких типов нельзя написать обычный map - зато можно написать contramap. И тогда стрелка разворачивается.

Контравариантный функтор имеет contramap вместо map. Если map принимает (A -> B) и превращает F<A> в F<B>, то contramap принимает (A -> B) и превращает F<B> в F<A>. Входная функция "разворачивается". Формально: контравариантный функтор из C в D - это ковариантный функтор из C^op в D.

Контравариантность в TypeScript встроена в систему типов. Параметры функций контравариантны: если `Dog extends Animal`, то `(Animal) => void` совместим с `(Dog) => void`, но не наоборот. Это не ошибка дизайна - это математическая необходимость. TypeScript просто реализует теорию категорий на уровне типов.

ТипВариантностьПочему
Array<T>КовариантныйПроизводит значения T
Promise<T>КовариантныйПроизводит значение T
Predicate<T>КонтравариантныйПотребляет значение T
Comparator<T>КонтравариантныйПотребляет два T
Serializer<T>КонтравариантныйПотребляет T, производит строку
(arg: T) => voidКонтравариантныйПараметр функции - потребитель

Для контравариантного функтора F, если f: A -> B, то F(f) имеет тип:

Бифунктор и профунктор: два аргумента

Некоторые типы параметризованы двумя аргументами: `Either<A, B>`, `Pair<A, B>`, `Map<K, V>`. Бифунктор - это функтор от двух аргументов. По каждому он ведёт себя как обычный ковариантный функтор. `bimap(f, g)` преобразует оба компонента одновременно.

Бифунктор B: C x D -> E - это функтор из произведения категорий C x D в E. Он функториален по каждому аргументу: B(f, id) - функтор по первому аргументу B(id, g) - функтор по второму аргументу B(f, g) = B(f, id) o B(id, g) = B(id, g) o B(f, id)

Особый случай: тип функции `A -> B` - это бифунктор. Но необычный: контравариантный по A (входу) и ковариантный по B (выходу). Такой бифунктор называется профунктором. Он описывает что-то, что одновременно потребляет один тип и производит другой. Оптика в Haskell (lens, prism, iso) строится именно на профункторах.

Резюме иерархии. Ковариантный функтор: map сохраняет стрелки. Контравариантный: contramap разворачивает. Бифунктор: bimap по двум аргументам. Профунктор: dimap - contramap по входу, map по выходу. Всё это - разные ответы на вопрос «как функтор относится к направлению стрелок». И все они живут в каждой строке кода на TypeScript, Rust, Haskell, Scala.

Функтор - это просто метод .map()

Функтор - это отображение между категориями на двух уровнях: объекты (типы) и морфизмы (функции), удовлетворяющее двум законам

.map() - только отображение на морфизмах. Функтор также включает отображение на объектах: сам контейнерный тип Array<T>, Option<T>. Без законов (identity и композиция) любой .map() с побочными эффектами или нарушающий порядок - не функтор. GHC использует именно законы для fusion-оптимизации: если законы нарушены, компилятор сломает программу молча.

Тип функции A -> B является профунктором. Какова его вариантность?

Ключевые идеи

  • **Функтор** F: C -> D - отображение между категориями, сохраняющее identity (F(id_A) = id_{F(A)}) и композицию (F(g o f) = F(g) o F(f))
  • **Ковариантный** функтор сохраняет направление стрелок: f: A -> B => F(f): F(A) -> F(B). Это Array.map, Promise.then, torch.vmap
  • **Контравариантный** функтор разворачивает стрелки: f: A -> B => F(f): F(B) -> F(A). Это Predicate, Comparator, параметры функций в TypeScript
  • **Бифунктор** - функтор от двух аргументов: bimap(f, g) преобразует оба компонента (Either, Pair)
  • **Профунктор** - бифунктор, контравариантный по входу и ковариантный по выходу: dimap. Основа lens-оптики в Haskell

Связанные темы

Функторы - мост между категориями и основа для более сложных конструкций:

  • Категории: объекты и морфизмы — Фундамент: функторы определены на категориях
  • Естественные преобразования — Морфизмы между функторами - следующий уровень абстракции

Вопросы для размышления

  • Какие ещё контейнерные типы в знакомом языке программирования являются функторами? Проверьте оба закона.
  • Почему тип функции A -> B контравариантен по A? Чем отличается потребитель от производителя в категорном смысле?
  • Если функтор F: C -> D и функтор G: D -> E, что такое G o F? Является ли это функтором из C в E?

Связанные уроки

  • ct-01 — Категории и морфизмы - фундамент для функторов
  • ct-03 — Естественные преобразования - морфизмы между функторами
  • aa-01-groups-intro — Гомоморфизм групп - частный случай функтора
  • la-02-dot-product — Линейные отображения - функторы категории Vect
  • aa-03-homomorphisms
  • plt-07-algebraic-types
Функторы: map как математическая структура

0

1

Войти