Теория категорий
Category Theory в CS
Почему TypeScript имеет intersection types? Почему async/await ведёт себя как монада? Почему линзы компонуются как функции? За всем этим стоит теория категорий. Это не случайность: лучшие конструкции в языках программирования - это категорные конструкции. Понять их математически - значит предсказывать, какие конструкции будут работать, ещё до написания кода.
- **Control.Lens в Haskell**: библиотека линз реализует профункторные оптики. Composable, typesafe, efficient - все свойства следуют из категорной теории. `(.)` - категорная композиция естественных преобразований
- **Idris, Agda, Coq**: языки с зависимыми типами - это реализации Мартин-Лёфовской теории типов, которая соответствует локально декартово замкнутым категориям. Доказательства = программы по Карри-Говарду
- **Scala Cats / Haskell mtl**: библиотеки эффектов реализуют Freyd-categories (монады с дополнительной структурой). Tagless Final - вложение в категорию T-алгебр
Предварительные знания
Types As Objects
Типы в языке программирования ведут себя как объекты категории: у них есть морфизмы (функции), произведения (пары), копроизведения (sum types), экспоненциалы (функциональные типы). Это не метафора - категория **Hask** (Haskell-типы) является **декартово замкнутой категорией** (cartesian closed category, CCC), и именно это объясняет, почему функциональное программирование работает.
**Декартово замкнутая категория** (CCC) - категория с: 1. терминальным объектом 1 2. конечными произведениями A × B 3. экспоненциалами B^A = (A → B), удовлетворяющими curry: Hom(X × A, B) ≅ Hom(X, B^A). Категория Hask - CCC. Каждый функциональный язык реализует CCC.
| Категорная конструкция | Тип данных | TypeScript пример |
|---|---|---|
| Терминальный объект 1 | Unit / void | undefined, null (приближение) |
| Инициальный объект 0 | Never / Void | never - пустой тип |
| Произведение A × B | Tuple / Record | [A, B], { a: A; b: B } |
| Копроизведение A + B | Union / Either | A | B, Either<A,B> |
| Экспоненциал B^A | Функциональный тип | (a: A) => B |
| Монада T | Тип с контекстом | Promise<A>, Option<A> |
**Изоморфизм Карри-Говарда**: типы ↔ логические формулы, программы ↔ доказательства. Product type соответствует конъюнкции (∧), Sum type - дизъюнкции (∨), функция - импликации (→), Never - ложи (⊥), Unit - истине (⊤). Написать функцию типа `(A, B) => A` - то же, что доказать P ∧ Q → P. Этот изоморфизм объясняет, почему системы типов и логика так похожи.
Соответствие Ламбека
Джоаким Ламбек в 1969-1980 показал, что декартово замкнутые категории соответствуют типизированному лямбда-исчислению: объекты = типы, морфизмы = замкнутые термы (с точностью до βη-эквивалентности). Это третья вершина треугольника Карри-Говарда-Ламбека. Любой CCC - модель лямбда-исчисления; любое типизированное лямбда-исчисление порождает CCC.
Почему curry/uncurry - это изоморфизм в CCC?
Denotational
**Денотационная семантика** - это способ придать программе математическое значение (денотат), не зависящее от деталей выполнения. Функция `f :: Int -> Int` - это математическая функция ℤ → ℤ. Нерекурсивные программы просто; рекурсия требует решения уравнений. Категорная теория (домены Скотта) предоставляет инструменты.
**Денотационная семантика** языка L - это функтор [[·]]: L → D из категории программ в математическую категорию D. Для Haskell D - категория CPO (complete partial orders) или DCPO: частично упорядоченных множеств с наименьшими верхними гранями направленных подмножеств. Рекурсия = наименьшая неподвижная точка по теореме Тарского.
**Категории доменов Скотта**: объекты - CPO (упорядоченные множества с наименьшим элементом ⊥ и направленными супремумами), морфизмы - непрерывные (Scott-continuous) функции, сохраняющие directed joins. Теорема Кнастера-Тарского: монотонная функция на полной решётке имеет наименьшую неподвижную точку. Это математическое основание рекурсии.
Современные альтернативы: **Axiomatic Semantics** (Флойд-Хоар), **Operational Semantics** (структурная операционная семантика Плоткина). Денотационная семантика - наиболее математически «чистая», но сложнее для конкурентности и эффектов. Категории используются во всех трёх: категории переходов, логики Хоара и т.д.
Денотационная семантика рекурсивной функции f = F(f) вычисляется как:
Profunctors
**Профунктор** (distributor, bimodule) - это «обобщённое соответствие» между двумя категориями. Функтор F: C → D можно рассматривать как профунктор через Hom_D(-, F(-)): C^{op} × D → Set. Профункторы обобщают как функторы, так и просто отношения. Они образуют **бикатегорию Prof**, мощную альтернативу Cat для многих задач.
**Профунктор** P: C ↛ D - это функтор P: D^{op} × C → Set. Тождественный профунктор id_C = Hom_C: C^{op} × C → Set. Композиция профункторов P: B ↛ C и Q: C ↛ D: (Q ∘ P)(b, d) = ∫^c P(b, c) × Q(c, d) (копредел над объектами C - «интеграл по концу»).
**Зачем профункторы**: они обобщают функторы и дают удобный язык для описания «соответствий» между данными. **Конец** (end) профунктора ∫_c P(c,c) - «глобальные элементы», естественные по c. Лемма Ёнеды через конец: Nat(F, G) ≅ ∫_c F(c) → G(c). Профункторы также основа теории **оптик** - компонуемых линз и призм.
| Концепция | Профункторное описание |
|---|---|
| Функтор F: C → D | Профунктор Hom_D(-, F(-)): C ↛ D |
| Естественные преобразования | Nat(F, G) = ∫_c Hom(F(c), G(c)) - конец |
| Сопряжение F ⊣ G | Hom_D(F(-), -) ≅ Hom_C(-, G(-)) как профункторы |
| Лемма Ёнеды | P(a, b) ≅ ∫_x Hom(a, x) → P(x, b) - профункторная формула |
Профунктор P: C ↛ D - это функтор из:
Optics
**Оптики** (optics) - обобщение «линз» из функционального программирования. Линза позволяет «смотреть» (get) и «модифицировать» (set/over) часть структуры данных в чисто функциональном стиле. Теория категорий даёт математическое основание: **профункторная оптика** - это точное категорное определение, унифицирующее линзы, призмы, траверсы и другие оптики.
**Профункторная оптика** (optic) типа O(S, T, A, B) - это естественное преобразование: для любого профунктора P: Set ↛ Set, функция O(S, T, A, B) = ∀P. P(A, B) → P(S, T). Линза (Lens) - это оптика, где P ограничен на «Strong» профункторы; призма (Prism) - на «Choice» профункторы. Это элегантная унификация через Yoneda.
| Оптика | Профунктор P | Что описывает |
|---|---|---|
| Lens S A | Strong P (с first/second) | Фокус на части произведения |
| Prism S A | Choice P (с left/right) | Фокус на ветви копроизведения |
| Traversal S A | Traversing P | Фокус на нескольких элементах |
| Getter S A | Forget r | Только чтение (get) |
| Setter S A | Star (->) | Только запись (over/set) |
**Экзистенциальная кодировка**: альтернативная формулировка оптик через экзистенциальные типы. Lens S T A B = ∃R. (S → R × A) × (R × B → T). Это «хранение остатка» R при разборке S на (R, A) и сборке обратно из (R, B). Профункторная и экзистенциальная кодировки изоморфны - что следует из леммы Ёнеды!
Теория категорий в программировании - это только про монады и функторы
Теория категорий пронизывает весь дизайн языков: системы типов (CCC), денотационная семантика (CPO), оптики (профункторы), эффекты (Freyd-categories), зависимые типы (локально декартово замкнутые категории)
Homotopy Type Theory (HoTT) - язык, где типы - это ∞-группоиды (объекты ∞-топоса). Rust ownership - аффинные типы из аффинной категории. Linear Haskell - линейная логика (symmetric monoidal closed categories). Каждая нетривиальная система типов - это какая-то категорная структура.
Что делает профункторная оптика более гибкой, чем просто пара get/set?
Ключевые идеи
- **Types as objects**: язык программирования с произведениями, суммами и функциями = декартово замкнутая категория (CCC). Curry/uncurry - изоморфизм из аксиомы экспоненциала
- **Денотационная семантика**: программа = морфизм в CPO; рекурсия = наименьшая неподвижная точка по Тарскому; семантический функтор [[·]] сохраняет категорную структуру
- **Профункторы** P: D^{op} × C → Set обобщают функторы и отношения; конец ∫ - мощный инструмент для формулировки леммы Ёнеды и естественных преобразований
- **Оптики** (линзы, призмы, траверсы) = профункторные естественные преобразования; компонуются как морфизмы; единая категорная конструкция за всеми видами оптик
Связанные темы
Теория категорий в CS опирается на все предыдущие концепции:
- Монады в программировании — Практическая основа: Maybe, List, IO - категорные монады на CCC типов
- Category Theory на собеседовании — Следующий урок: как объяснить все эти концепции на интервью и в коде
Вопросы для размышления
- TypeScript intersection type `A & B` и union type `A | B` соответствуют произведению и копроизведению. Но `A & B` в TS - это «оба свойства», что больше похоже на SET-теоретическое пересечение. Когда intersection type - это произведение, а когда - пересечение? Одно ли это?
- Реализуйте Prism<S, A> для Maybe: Prism<Maybe<A>, A> с операциями preview (Maybe<A> → Option<A>) и review (A → Maybe<A>). Как это связано с копроизведением?
- Профунктор dimap позволяет «контрамапить» вход и «мапить» выход. Чем это отличается от просто Functor (только map)? Приведите пример, где нужен именно dimap, а не map.