scalabook

Форк
0
/
category_theory.md 
530 строк · 33.4 Кб

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

Теория категорий — раздел математики, изучающий свойства отношений между математическими объектами, не зависящие от внутренней структуры объектов.

Категория C — это:

  • класс объектов \(Ob_{C}\)
  • для каждой пары объектов A, B задано множество морфизмов (или стрелок) \(Hom_{C}(A,B)\), причём каждому морфизму соответствуют единственные A и B
  • для пары морфизмов \(f \in Hom(A,B)\) и \(g \in Hom(B,C)\) определена композиция \(g \circ f \in Hom(A,C)\) Commutative diagram for morphism
  • для каждого объекта A задан тождественный морфизм \(id_{A} \in Hom(A,A)\) (стрелка от объекта к самому себе)

причём выполняются две аксиомы:

  • операция композиции ассоциативна: если есть три морфизма, f, g и h, которые могут быть скомпонованы (то есть, их типы согласованы друг с другом), то порядок компоновки не имеет значения: \(h \circ (g \circ f) = (h \circ g) \circ f\)
  • тождественный морфизм действует тривиально: \(f \circ id_{A} = id_{B} \circ f = f\) для \(f \in Hom(A,B)\)

Category laws

В Scala это определение можно выразить так:

def compose[A, B, C](f: A => B, g: B => C): A => C = f andThen g
def id[A]: A => A = a => a

А соответствующие законы вот так (если задана операция assert

, способная проверять функции на равенство, например, на какой-то выборке):

trait Laws[A, B, C, D]:
def associativityLaw(f: A => B, g: B => C, h: C => D): Unit =
assert(compose(compose(f, g), h), compose(f, compose(g, h)))
def identityLaw(f: A => B): Unit =
assert(compose(f, id), f)
assert(compose(id, f), f)

Краткий итог

Категория состоит из объектов и стрелок (морфизмов). Стрелки могут быть скомпонованы, их композиция ассоциативна. Каждый объект имеет единичную стрелку, которая служит в качестве единицы композиции.

Инициальный и терминальный объекты

Инициальный объект — это объект, от которого к любому объекту категории исходит ровно один морфизм.

Двойственным образом определяется терминальный или универсально притягивающий объект — это такой объект, в который из любого объекта категории существует единственный морфизм.

Рассмотрим пример:

Если представить, что класс объектов \(Ob_{C}\) - это Scala тип Byte

, а морфизм - это отношение "меньше" (<
), то в такой категории инициальным объектом будет -128
(инициальный объект "меньше" всех остальных объектов - исходит стрелка), а терминальным будет 127
(из каждого объекта существует стрелка "меньше" в этот объект).

Назовем упорядоченной категорией категорию, где объект a предшествует объекту b, если существует стрелка (морфизм) от a к b. Инициальный объект в частично упорядоченном множестве — это его наименьший элемент. Некоторые частично упорядоченные множества, такие как все целые числа (и положительные, и отрицательные), не имеют инициального объекта.

Рассмотрим ещё один пример:

Если представить, что класс объектов \(Ob_{T}\) - это система типов Scala, а морфизм - это отношение "наследует" (A extends B

или \(A \mapsto B\)), то в такой категории инициальным объектом будет Nothing
(от него к любому объекту категории исходит ровно один морфизм или Nothing
наследует от любого типа в Scala), а терминальным - Any
(из каждого объекта существует стрелка "наследует" в этот объект).

Глобальный элемент

В теории категорий говорят, что x

является глобальным элементом для a
, если он является стрелкой \(1 \overset{x}{\rightarrow} a\), где 1
- терминальный объект.

В теории типов, x: A

означает, что x
является типом A
.

Система типов Scala различает x: A

и x: () => A
. Однако, в категорной семантике они обозначают одно и то же. В дальнейшем глобальный элемент будем обозначать как x: A
.

Поскольку стрелок от любого другого объекта к инициальному объекту нет, то нет и стрелок от терминального объекта к нему. Поэтому инициальный объект не имеет элементов. И в Scala нет типа для инициального объекта. Терминальный объект имеет только один элемент, так как от него к самому себе идет единственная стрелка, 1 → 1

; поэтому иногда его называют синглетоном. В Scala терминальный объект - это Unit
с единственным элементом - ()
.

Двойственность

Для категории C можно определить двойственную категорию \({\mathcal {C}}^{{op}}\), в которой:

  • объекты совпадают с объектами исходной категории;
  • морфизмы получаются «обращением стрелок»: \({\mathrm {Hom}}_{{{\mathcal {C}}^{{op}}}}(B,A) \) \(\simeq {\mathrm {Hom}}_{{{\mathcal {C}}}}(A,B)\)

Двойственная категория автоматически удовлетворяет определению категории, если мы одновременно с обращением стрелок переопределим композицию. Если композицией исходных морфизмов \(f \in Hom(A,B)\) и \(g \in Hom(B,C)\) был морфизм \(h \in Hom(A,C)\) с \(h = g \circ f\), то композицией обращенных морфизмов \(f^{op} \in Hom(B,A)\) и \(g^{op} \in Hom(C,B)\) будет морфизм \(h^{op} \in Hom(C,A)\) с \(h^{op} = f^{op} \circ g^{op}\). Отметим, что обращение тождественной стрелки совпадает с ней самой.

В Scala это определение можно выразить так:

def cocompose[A, B, C](f: B => A, g: C => B): C => A = g andThen f

Принцип двойственности гласит, что для любого утверждения теории категорий можно сформулировать двойственное утверждение с помощью обращения стрелок, при этом истинность утверждения не изменится. Часто двойственное понятие обозначается тем же термином с приставкой ко-.

Терминальный объект — это инициальный объект в двойственной категории.

Композиция, пост-композиция, пред-композиция

Композиция

Имея стрелку f от a к b и стрелку g от b к c, их композиция представляет собой стрелку, идущую непосредственно от a к c. Другими словами, если имеются две стрелки, цель одной из которых совпадает с источником другой, то всегда можно скомпоновать их, чтобы получить третью стрелку. Обозначается как: \(h = g \circ f\)

Ассоциативность

Предположим, что удалось разложить g на \(j \circ k\). Тогда \(h = (j \circ k) \circ f\) Желательно, чтобы это разложение было таким же, как и \(h = j \circ (k \circ f)\). При этом должна существовать возможность заявить, что h была декомпозирована на три более простые функции \(h = j \circ k \circ f\) и не нужно отслеживать, какая декомпозиция была первой. Это свойство называется ассоциативностью композиции.

Пост-композиция

Композиция является источником двух отображений стрелок, называемых пред-композицией и пост-композицией. Когда стрелка f является пост-компонуемой со стрелкой h, получается стрелка \(f \circ h\). Конечно, f можно пост-компоновать только со стрелками, целью которых является источник для f. Пост-композиция посредством f записывается как \(f \circ -\), оставляя пустоту для h. Таким образом, стрелка f : a → b индуцирует отображение стрелок \(f \circ -\), которое отображает стрелки, идущие к a, в стрелки, идущие к b.

Пост-композиция позволяет смещать фокус с одного объекта на другой.

Пред-композиция

Двойственным образом, можно пред-композировать f, или применить \(- \circ f\) к стрелкам, исходящим из b, сопоставляя их со стрелками, исходящими из a. Пред-композиция позволяет смещать перспективу от одного наблюдателя к другому.

В программировании, исходящая стрелка интерпретируется как извлечение данных из источника. Входящая стрелка интерпретируется как создание цели. Исходящие стрелки определяют интерфейс, входящие стрелки определяют конструкторы.

На Scala пред- и пост-композиции можно изобразить так:

trait Arrow[A, B]:
extension(f: A => B)
def postCompose[X](g: X => A): X => B = x => f(g(x))
def preCompose[X](g: B => X): A => X = a => g(f(a))

Применение функции и тождественность

Применение функции

Рассмотрим стрелку от терминального объекта 1 к (типу) a. Это какой-то элемент x типа a. Можно записать это как \(1 \overset{x}{\rightarrow} a\). Рассмотрим стрелку от a к b: \(a \overset{f}{\rightarrow} b\). Эти две стрелки являются компонуемыми (они соприкасаются на объекте a), и их композиция — это стрелка y от 1 к b. Другими словами, y является элементом из b. Можно записать это так: \(y = f \circ x\). Мы используем f для отображения элемента из a к элементу из b. Такое действие называется применение функции f к x:

trait Arrow[A, B]:
extension(f: A => B)
def apply(a: A): B = f(a)

Тождественность

Каждый объект имеет специальную стрелку, называемую тождественной (или тождественностью), которая оставляет объект неизменным. Это означает, что когда вы компонуете тождественную стрелку с любой другой стрелкой, входящей или исходящей, то получаете ту же стрелку. Тождественная стрелка ничего не делает с другой стрелкой.

Тождественная стрелка на объекте a обозначается как \(id_{a}\). Так что, если имеется стрелка \(f : a \to b\), то можно скомпоновать ее с тождественностями с обеих сторон: \(id_{b} \circ f = f = f \circ id_{a}\)

trait Arrow[A, B]:
def identity(a: A): A = a

Возьмем элемент \(x : 1 \to a\) и скомпонуем его с \(id_{a}\). Результат \(id_{a} \circ x = x\) означает, что тождественность оставляет элементы неизменными.

Изоморфизм, эндоморфизм, автоморфизм

Изоморфизм

Морфизм \(f \in Hom(A,B)\) называется изоморфизмом, если существует такой морфизм \(g \in Hom(B,A)\), что \({\displaystyle g \circ f = \mathrm {id}_{A}}\) и \({\displaystyle f \circ g = \mathrm {id}_{B}}\). Это отношение записывается как \(g = f^{−1}\) (произносится, как обратная к f). Стрелка \(f^{−1}\), другими словами, отменяет результат действия стрелки f. Два объекта, между которыми существует изоморфизм, называются изоморфными (записывется \(a \cong b\)). В частности, тождественный морфизм является изоморфизмом, поэтому любой объект изоморфен сам себе.

В программировании, два изоморфных типа имеют одинаковое внешнее поведение. Один тип может быть реализован через другой, и наоборот. Одно можно заменить другим без изменения поведения системы (за исключением, возможно, производительности). Например, многие коллекции в Scala изоморфны друг другу, скажем, List

и Vector
.

Когда мы говорим, что инициальный (или терминальный) объект единственен с точностью до изоморфизма, то имеется в виду, что любые два инициальные (или терминальные) объекты изоморфны. Это легко продемонстрировать. Предположим, что \(i_{1}\) и \(i_{2}\) — инициальные объекты в одной и той же категории. Поскольку \(i_{1}\) инициальный, то существует единственный морфизм f от \(i_{1}\) к \(i_{2}\). Аналогично, поскольку \(i_{2}\) инициальный, то существует единственный морфизм g от \(i_{2}\) к \(i_{1}\). Что можно сказать о композиции этих морфизмов? Композиция \(g \circ f\) должна быть морфизмом от \(i_{1}\) к \(i_{1}\). Но \(i_{1}\) является инициальным объектом, так что существует ровно один морфизм от \(i_{1}\) к \(i_{1}\) и, поскольку начало и конец стрелки совпадают, эта вакансия уже занята тождественным морфизмом. Следовательно, они должны совпадать: морфизм \(g \circ f\) является тождественным. Аналогично, морфизм \(f \circ g\) также совпадает с тождественным, поскольку может быть всего один морфизм от \(i_{2}\) к \(i_{2}\). Таким образом, f и g взаимнообратны, а два инициальных объекта изоморфны.

Естественность

Изменение точки зрения сохраняет взаимную поддержку, установленную изоморфизмом. Если две стрелки были напарниками с точки зрения x, они по-прежнему остаются напарниками и с точки зрения y. Т.е. не имеет значения, делаете ли вы сначала пред-композицию с помощью g (переключение точки зрения), а затем пост-композицию с помощью f (переключение фокуса), или сначала пост-композицию с помощью f, а затем пред-композицию с g. Символически это записывается так: \((− \circ g) \circ (f \circ −) = (f \circ −) \circ (− \circ g)\) - это называется условием естественности. Смысл этого равенства раскрывается, если применить его к морфизму \(h : x \to a\). Обе стороны оцениваются как \(f \circ h \circ g\): \(y \overset{g}{\rightarrow} x \overset{h}{\rightarrow} a \overset{f}{\rightarrow} b\)

С точки зрения изоморфной пары условие естественности выглядит так: \((− \circ f^{−1}) \circ (g \circ −) = (g \circ −) \circ (− \circ f^{−1})\). Что сводится к \(b \overset{f^{-1}}{\rightarrow} a \overset{h}{\rightarrow} x \overset{g}{\rightarrow} y\).

Эндоморфизм

Морфизмы, в которых начало и конец совпадают, называют эндоморфизмами. Множество эндоморфизмов \({\displaystyle \mathrm {End}(A) = \mathrm {Hom}(A,A)}\) является моноидом относительно операции композиции с единичным элементом \({\displaystyle \mathrm {id}_{A}}\).

Автоморфизм

Эндоморфизмы, которые одновременно являются изоморфизмами, называются автоморфизмами. Автоморфизмы любого объекта образуют группу автоморфизмов \({\displaystyle \mathrm {Aut} (A)}\) по композиции.

Мономорфизм, эпиморфизм, биморфизм

Мономорфизм — это морфизм \(f \in {\mathrm {Hom}}(A,B)\) такой, что для любых \(g_{1}, g_{2} \in {\mathrm {Hom}}(X,A)\) из \(f \circ g_{1} = f \circ g_{2}\) следует, что \(g_{1} = g_{2}\). Композиция мономорфизмов есть мономорфизм.

Эпиморфизм — это такой морфизм \(f \in {\mathrm {Hom}}(A,B)\), что для любых \(g_{1}, g_{2} \in {\mathrm {Hom}}(B,X)\) из \(g_{1} \circ f = g_{2} \circ f\) следует, что \(g_{1} = g_{2}\). Композиция эпиморфизмов есть эпиморфизм.

Биморфизм — это морфизм, являющийся одновременно мономорфизмом и эпиморфизмом. Любой изоморфизм есть биморфизм, но не любой биморфизм есть изоморфизм.

Произведение и сумма объектов

Произведение

Произведение (пары) объектов A и B — это объект \(A \times B\) с морфизмами \(p_{1}:A \times B \to A\) и \(p_{2}:A \times B \to B\) такими, что для любого объекта C с морфизмами \(f_{1}: C \to A\) и \(f_{2}: C \to B\) существует единственный морфизм \(g: C \to A \times B\) такой, что диаграмма, изображённая ниже, коммутативна.

Product

Морфизмы \(p_{1}: A \times B \to A\) и \(p_{2}: A \times B \to B\) называются проекциями.

Коммутативность, объединители, ассоциативность и функториальность

Произведение удовлетворяет правилу коммутативности с точностью до изоморфизма: \(a \times b \cong b \times a\).

Обратимая стрелка, свидетельствующая об изоморфизме между \(1 \times a\) и a, называется левым объединителем (или унитором, unitor): \(\lambda : 1 \times a \to a\). Аналогично определяется правый объединитель: \(\rho : a \times 1 \to a\). Ассоциатором называется изоморфизм: \(\alpha : (a \times b) \times c \to a \times (b \times c)\).

Предположим, что имеются стрелки, которые отображают a и b к некоторым \(\acute{a}\) и \(\acute{b}\): \(f: a \to \acute{a}, g: b \to \acute{b}\). Композицию этих стрелок с проекциями \(p_{1}\) и \(p_{2}\), соответственно, можно использовать для определения отображения между произведениями: \(a \times b \overset{f \times g}{\rightarrow} \acute{a} \times \acute{b}\). Это свойство произведения называется функториальностью. Оно позволяет трансформировать два объекта внутри произведения, чтобы получить новое произведение.

Произведение удовлетворяет следующим свойствам:

  • \(1 \times a \cong a\)
  • \(a \times b \cong b \times a\)
  • \((a \times b) \times c \cong a \times (b \times c)\)

и оно функториально. Категория с таким типом операции называется симметричной моноидальной.

Пример

В Scala примером произведений могут служить кортежи:

val product: (String, Int) = ("product", 42)
val (str, num) = product

Коммутативность:

def swap[A, B](product: (A, B)): (B, A) = (product._2, product._1)

Ассоциативность:

def assoc[A, B, C](product: ((A, B), C)): (A, (B, C)) = (product._1._1, (product._1._2, product._2))

Функториальность:

def functoriality[A, B, C, D](ab: (A, B), f: A => C, g: B => D): (C, D) =
(f(ab._1), g(ab._2))

Копроизведение (Тип-сумма)

Двойственно определяется сумма или копроизведение A + B объектов A и B. Соответствующие морфизмы \(\imath_{A}: A \to A + B\) и \(\imath_{B}: B \to A + B\) называются вложениями. Несмотря на своё название, в общем случае они могут и не быть мономорфизмами.

Рассмотрим на примере тип-суммы Bool

:

enum Bool:
case True, False
val x: Bool = Bool.True

Функции True

и False
, которые использованы в определении Bool
, называются конструкторами данных. Их можно использовать для создания конкретных термов, как в примере выше.

Рассмотрим отображение из типа-суммы. Каждая функция Bool -> A

производит пару элементов типа A
.

def apply[A](f: Bool => A): (A, A) =
(f(Bool.True), f(Bool.False))

Любая функция от Bool

к A
не только производит, но и эквивалентна паре элементов из A
. Другими словами, пара элементов однозначно определяет функцию от Bool
:

def f[A](b: Bool, x: A, y: A): A =
b match
case Bool.True => x
case Bool.False => y

Тип-сумму можно расширить не большее количество объектов, используя перечисления.

Коммутативность, ассоциативность и функториальность

Тип-сумма удовлетворяет правилу коммутативности с точностью до изоморфизма: \(a + b \cong b + a\).

А также ассоциативности: \(a + (b + c) \cong (a + b) + c\).

Предположим, что имеются стрелки, которые отображают a и b к некоторым \(\acute{a}\) и \(\acute{b}\): \(f: a \to \acute{a}, g: b \to \acute{b}\). Композицию этих стрелок с конструкторами Left и Right, соответственно, можно использовать для определения отображения между суммами. Пара стрелок, \((Left \circ f, Right \circ g)\) однозначно определяет стрелку \(h: a + b → \acute{a} + \acute{b}\). Это свойство суммы называется функториальностью. Можно представлять себе, что это позволяет преобразовывать два объекта внутри суммы и получить новую сумму. Мы также говорим, что функториальность позволяет нам поднять пару стрелок, чтобы оперировать суммами.

Очевидно, что из-за ассоциативности функториальность распространяется на тип-суммы большей размерности.

Тип-сумма удовлетворяет следующим свойствам:

  • \(a + 0 \cong a\)
  • \(a + b \cong b + a\)
  • \((a + b) + c \cong a + (b + c)\)

и она функториальна. Категория с таким типом операции называется симметричной моноидальной.

Пример

В Scala примером копроизведений (тип сумм) могут служить такие структуры, как: Option, Either и List.

enum Option[+A]:
case Some(get: A)
case None
enum Either[+E, +A]:
case Left(value: E)
case Right(value: A)
enum List[+A]:
case Nil
case Cons(head: A, tail: List[A])

Данные типы изоморфны друг другу, например:

enum List[+A]:
self =>
case Nil
case Cons(head: A, tail: List[A])
val toOption: Option[List[A]] =
self match
case Nil => Option.None
case _ => Option.Some(self)
val toEither: Either[Unit, List[A]] =
self match
case Nil => Either.Left(())
case _ => Either.Right(self)

Законы коммутативности и ассоциативности на примере Either

:

// Коммутативность
def f[A, B](either: Either[A, B]): Either[B, A] =
either match
case Left(a) => Right(a)
case Right(b) => Left(b)
def fReverse = f
// Ассоциативность
def g[A, B, C](either: Either[Either[A, B], C]): Either[A, Either[B, C]] =
either match
case Left(Left(a)) => Left(a)
case Left(Right(b)) => Right(Left(b))
case Right(c) => Right(Right(c))
def gReverse[A, B, C](
either: Either[A, Either[B, C]]
): Either[Either[A, B], C] =
either match
case Left(a) => Left(Left(a))
case Right(Left(b)) => Left(Right(b))
case Right(Right(c)) => Right(c)

Функториальность Either

:

def map[A, B, C, D](either: Either[A, B], f: A => C, g: B => D): Either[C, D] =
either match
case Left(a) => Left(f(a))
case Right(b) => Right(g(b))

Примеры в различных категориях

Если произведение и копроизведение существуют, то они определяются однозначно с точностью до изоморфизма.

  • Пример: В категории Set произведение A и B — это прямое произведение в смысле теории множеств \(A \times B\), а сумма — дизъюнктное объединение \(A \sqcup B\).
  • Пример: В категории колец Ring сумма — это тензорное произведение \(A \otimes B\), а произведение — прямая сумма колец \(A \oplus B\).
  • Пример: В категории VectK (конечные) произведение и сумма изоморфны — это прямая сумма векторных пространств \(A \oplus B\).

Алгебра типов

Для произведения и ко-произведения выполняется дистибутивный закон a * (b + c) = a * b + a * c

с точностью до изоморфизма:

val leftside: (A, Either[B, C]) = ???
val rightside: Either[(A, B), (A, C)] =
leftside match
case (a, Left(b)) => Left((a, b))
case (a, Right(c)) => Right((a, c))
rightside match
case Left((a, b)) => (a, Left(b))
case Right((a, c)) => (a, Right(c))

Ссылки:

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.