Теория категорий
Функторы: 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 functor | promise.then(f) |
| **Option<T>** / Maybe | Nullable functor | opt.map(f) |
| **torch.vmap(f)** | Batch functor | f: Tensor -> Tensor поднимается до батча |
| **jax.vmap(f)** | Batch functor | vectorized 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