Ранее мы выяснили, что монадные возможности присущи эндофункторам, основанных на определённых ковариантных обобщённых типах. Обобщённые же типы соответствуют алгебраическим выражениям, собранным из сумм, произведений и экспоненциалов типов.

В этой части обзора мы увидим, почему конструкторы типов связаны с алгебраическими операциями, и как сама эта связь порождает монады и комонады.

Оглавление обзора
  1. Hom-типы

  2. Функторы

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

  4. Монады

  5. Пределы и сопряжения функторов (данная публикация)

Содержание

Универсальные свойства типов

В одной из предыдущих частей обзора был продемонстрирован функтор, связывающий две разные категории одного и того же объекта — категории типов \star. Только в начальной категории морфизмами выступали конструкторы типов F[_], а в конечной — эндофункторы Functor[F]. Полученный функтор сохранял композицию конструкторов типов, позволяя из любых Functor[F] и Functor[G] создавать Functor[G ∘ F]. А это, в свою очередь, означает, что если имеются реализации эндофункторов для каких-либо базовых F[_], у нас автоматически есть эндофункторы для абсолютно любых контейнерных типов!

Открытым остаётся вопрос, какие конструкторы типов можно считать настолько «базовыми», что композируя их, можно получить вообще всё. В других моих статьях о теории типов неоднократно упоминалось об их алгебраической природе, о том, что в их основе лежат элементарные математические операции — сумма, произведение и экспоненциал (а также понятия нуля и единицы). Однако, мне приходилось всячески избегать объяснения, почему происходит именно так. Дело в том, что ключ к понимаю этого лежит в теории категорий, так что только сейчас настало время разобраться с этим вопросом.

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

В качестве примера напомню универсальное свойство произведения объектов:

произведением двух объектов A и B выбранной категории называется такой объект A \times B с морфизмами p_A: A \times B \rightarrow A и p_B: A \times B \rightarrow B, что для любых объекта C и морфизмов f: C \rightarrow A и g: C \rightarrow B будет существовать единственный морфизм u: C \rightarrow A \times B, делающий такую диаграмму коммутирующей:

Универсальное свойство произведения.
Универсальное свойство произведения.

Определяемый объект A \times B, также как и все кандидаты C, имеют аналогичную пару морфизмов, идущих от них к A и B. Но среди всех кандидатов выделяется лишь один универсальный объект A \times B — его универсальный морфизм u будет единственным для любого другого кандидата. Все остальные кандидаты будут либо сложнее (например, морфизм A \times B \rightarrow A \times B \times X будет не единственным), либо диаграмма не будет коммутировать (по сути, любой кандидат, но выбранные для него морфизмы f и g не полиморфны по A, или B).

Определение любого универсального свойства в категории имеет такую структуру:

  • обозначаются объекты, через которые формулируется универсальный (для произведения и суммы — это просто пара объектов A, B);

  • выбирается семейство кандидатов с определёнными возможностями (в примере выше — пары морфизмов f, g или p_A, p_B);

  • для каждого кандидата определяется морфизм u, связывающий его с целевым универсальным объектом;

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

Как перечисленные пункты описать в терминах катего��ии типов? «Возможности» кандидата — это некие преобразования, связывающие его с объектами подкатегории, фиксирующими формулировку универсального свойства. А требование коммутируеммости диаграммы очень напоминает условие естественности этого преобразования!

Конечно же, тут подразумевается некая функториальность для конструктора типов F[_, ...]. Его параметры даже могут иметь разную вариантность, поэтому в общем случае придётся иметь дело с мультипрофункторами, но в этой статье мы будем иметь дело, в первую очередь, с ковариантными функторами и бифункторами.

Конусы и ко-конусы

Давайте сперва посмотрим, как универсальные свойства выражаются с помощью естественных преобразований в некоторой категории \mathcal{C}. Объекты, через которые формулируется универсальный объект, образуют подкатегорию в \mathcal{C}. Для произведения объектов a и b такая подкатегория будет дискретной, содержащей только эти два объекта и их тождественные морфизмы. Но в случае других универсальных свойств устройство подкатегории может быть и сложнее.

Выбор конкретной подкатегории в \mathcal{C} удобно выразить с помощью функтора, встраивающего в \mathcal{C} некую модельную категорию \mathcal{I}. В случае универсального свойства произведения это и будет дискретная категория двух индексов, скажем, и . Тогда функтор F_{ab}: \mathcal{I} \rightarrow \mathcal{C} и будет отвечать за выбор конкретных a и b в \mathcal{C}.

Ещё нам нужно выбрать объект-кандидат . Поручим это константному функтору \Delta\,c: \mathcal{I} \rightarrow \mathcal{C}. Естественное преобразование \Delta\,c \rightsquigarrow F называется конусом функтора :

{\rm Cone}_F(c): \Delta c \leadsto F
Вы же наверняка заметили, что Ёжик строил вовсе не пирамидку!
Вы же наверняка заметили, что Ёжик строил вовсе не пирамидку!

Естественность преобразования особо важна для случаев, когда в модельной категории \mathcal{I} есть другие морфизмы помимо тождественных (например, 1 \rightarrow 2), и функтор F_{ab} переносит их в также нетождественные морфизмы (a \rightarrow b).

Образ функтора F образует подкатегорию F_{ab}\; \mathcal{I} \subset \mathcal{C}, являющуюся «основанием» этого конуса, а объект c: \mathcal{C} будет его вершиной:

«Тело» конуса образуют морфизмы от вершины к «основанию».
«Тело» конуса образуют морфизмы от вершины к «основанию».

Для универсального свойства суммы объектов получим дуальную картину. Напомню,

суммой двух объектов A и B выбранной категории называется такой объект A + B с морфизмами i_A: A \rightarrow A + B и i_B: B \rightarrow A + B, что для любых объекта D и морфизмов f: A \rightarrow D и g: B \rightarrow D будет существовать единственный морфизм u: A + B \rightarrow D, делающий такую диаграмму коммутирующей:

Универсальное свойство суммы.
Универсальное свойство суммы.

В сравнении с произведением, объекты на диаграмме сохранили своё положение, но все морфизмы оказались обращены. Кандидаты в сумму объектов описываются дуальной конструкцией, называемой ко-конусом:

{\rm Cocone}_F(c): F \rightsquigarrow \Delta c

На диаграмме получаем как бы «перевёрнутый конус»:

Коконус — это такой же конус, но морфизмы направлены уже от основания к вершине.
Коконус — это такой же конус, но морфизмы направлены уже от основания к вершине.

Обратите внимание, что и моделирующая категория \mathcal{I} и функтор F_{ab} остались теми же, что и для произведения типов! Изменилось только направление естественного преобразования.

Кандидаты в универсальные объекты можно выразить либо через конусы, либо ко-конусы. Осталось только выбрать среди них тот самый универсальный (если он вообще существует).

Пределы и копределы

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

В формулировке универсального свойства упоминаются морфизмы, связывающие кандидатов и универсальные объекты так, что при этом не ломаются (ко)конусы («…делают диаграмму коммутирующей…»). Эти морфизмы образуют отношение частичного порядка среди кандидатов. Тогда может получиться так, что среди всех кандидатов выделяется «особенный», связанный с другими единственным (для каждого) универсальным морфизмом. Если такой объект существует (а это возможно не всегда!), его (ко)конус, и, следовательно, он сам будут считаться «универсальными». Универсальный (ко)конус зависит лишь от функтора F:\: \mathcal{I} \rightarrow \mathcal{C}, он носит название (ко)предел функтора F и обозначается как {\rm Lim}\,F ({\rm Colim}\,F).

Предел и копредел можно также трактовать как функторы, действующие из категории функторов: {\rm Lim,\, Colim}:\: \mathcal{C}^\mathcal{I} \rightarrow \mathcal{C}. В некотором смысле они противоположны диагональному функтору \Delta:\: \mathcal{C} \rightarrow \mathcal{C}^\mathcal{I}.

Чуть позднее мы рассмотрим этот аспект подробнее, а сейчас вернёмся к формализации (ко)пределов.

Итак, предельное универсальное свойство утверждает, что «…для каждого кандидата существует единственный морфизм в предел...». Очевидно, что речь идёт о взаимно-однозначном соответствии кандидатов и морфизмов между ними и пределом. Кандидаты представлены своими конусами, которые можно рассматривать как контравариантный функтор

\mathrm{Cone}_F:\; \mathcal{C}^{op} \rightarrow \mathrm{Hom}_{\mathcal{C}^{\mathcal{I}}}(\Delta\, \_,\, F)

Справа от стрелки у нас стоит так категория, объектами которой будут индексированные c:\mathcal{C} совокупности естественных преобразований \Delta c \rightsquigarrow F. Символ подчёркивания обозначает место, куда нужно «подставить» объект исходной категории, но, конечно же, подразумевается не столько сопоставление объектов, сколько контравариантное отображение морфизмов между ними.

С другой же стороны у нас универсальные морфизмы {\rm Hom}_{\mathcal{C}}(c,\, {\rm Lim}\, F), для которых получаем похожий контравариантный функтор:

U_{\mathrm{Lim}F}:\; \mathcal{C}^{op} \rightarrow {\rm Hom}_{\mathcal{C}}(\_,\, {\rm Lim}\, F)

Часто (в том числе у Милевски) предполагается, что категории \mathcal{C} и \mathcal{C}^{\mathcal{I}} являются малыми, так что морфизмы между их объектами образуют множества. Тогда оба hom-функтора не только исходят из одной и той же категории, но и приводят в одинаковую категорию множеств: \mathrm{Cone}_F,\; U_{\mathrm{Lim}F}: \mathcal{C}^{op} \rightarrow \mathcal{Set}. В этом случае, требуемую «взаимную однозначность» можно выразить через естественный изоморфизм этих функторов (каждому кандидату по универсальному морфизму!):

{\rm Cone}_F \cong \; U_{Lim\ F}
Пределу функтора  соответствует такой естественный изоморфизм контравариантных функторов. Их образы считаются подкатегориями множеств.
Пределу функтора F соответствует такой естественный изоморфизм контравариантных функторов. Их образы считаются подкатегориями множеств.

В компонентах получается такая диаграмма:

Оба функтора ставят в соответствие объекту  множества морфизмов в разных категориях. Красной стрелкой обозначена одна компонента естественного изоморфизма — пара обычных встречных функций высшего порядка между множествами.
Оба функтора ставят в соответствие объекту c:\; \mathcal{C}^{op} множества морфизмов в разных категориях. Красной стрелкой обозначена одна компонента естественного изоморфизма — пара обычных встречных функций высшего порядка между множествами.

В программировании мы работаем не с множествами, а с типами, но принципиально это не меняет картину. Кроме того, в теории категории зачастую находятся возможности вообще не опускаться до использования множеств — тот же предел функтора можно описать как изоморфизм специальных категорий запятой. Тема интересная, но похоже, что данный обзор и так сильно перегружен теорией…

Предпучки

Подобные контравариантные функторы (отображающие объекты категории во множество морфизмов), называются предпучками. Тогда естественные изоморфизмы выше — это условия представимости предпучка (ко)конусов в виде морфизмов в предел функтора. Понятие «представимости» имеет большое значение в теории категорией и мы с ней ещё столкнёмся в данном обзоре.

Теория пучков сейчас активно развивается и «её инструментарий может оказаться очень полезным». Впрочем, этой фразой обычно и завершаются все упоминания предпучков))).

(Если кому-то интересно, то для того, чтобы называться полноценными «пучками», предпучкам не достаёт дополнительных ограничений… без которых программистам и так не плохо)))

В свою очередь, для копредела имеем естественный изоморфизм уже ковариантных функторов:

\begin{eqnarray}\mathrm{Cocone}_F&:\;& \mathcal{C} \rightarrow \mathrm{Hom}_{\mathcal{C}^{\mathcal{I}}}(F,\, \Delta\, \_)\\U^{Colim\ F}&:\;& \mathcal{C} \rightarrow {\rm Hom}_{\mathcal{C}}({\rm Colim}\, F,\, \_)\\\\&& {\rm Cocone}_F \cong \; U^{Colim\ F}\end{eqnarray}

В этих естественных изоморфизмах слева расположена совокупность (ко)конусов, индексированная только функтором F, а справа — только его (ко)предел. По сути, мы получаем уравнения для поиска (ко)предела функтора, но решать их в общем случае не просто.

(Ко)пределы в категории типов

Как вообще можно построить новые типы? Исходно у нас нет возможности сделать что-то интересное с одним единственным типом, кроме как вернуть его же. Но можно попробовать скомбинировать пару существующих типов в какой-то новый. Получится функциональное отображение, на вход которому подаётся два типа, а на выходе получаем один новый. Это просто «двухдырочные» конструкторы типов вида (*, ) => . Их можно трактовать как функторы из произведения категорий типов в неё же: \star \times \star \rightarrow \star. Произведение двух категорий типов моделируется дискретной категорией \mathcal{I} = 2, состоящей из двух индексов и , так что наши бифункторы можно записать как F_{ab}: \star^2 \rightarrow \star. Конусами этих бифункторов будут естественные преобразования c компонентами, индексированными двумя объектами \mathcal{I}:

type Cone2[A, B] = [-C] =>> ( // C - кандидат
  C => A, // первая компонента естественного преобразования
  C => B  // вторая компонента естественного преобразования
)

С другой стороны у нас hom-тип, описывающий морфизмы из кандидата в предел:

type ULim[A, B] = [-C] =>> C => Lim[A, B]

Нам нужно выяснить, каким же может быть Lim[A, B], чтобы эти контравариантные функторы для Cone2[A, B][-_] и ULim[A, B][-_] образовывали естественный изоморфизм

infix type ≅[F[_], G[_]] = (
  right: F ~> G,
  left : G ~> F,
)

summon[Cone2[A, B] ≅ ULim[A, B]]

К счастью, мы заранее знаем, что пределом будет произведение типов. В этом мы убедимся, реализовав изоморфизм:

type Lim[A, B] = A × B // (A, B)

given limIso: [A, B] => Cone2[A, B] ≅ ULim[A, B] = (
  right = [C] => (cone: (C => A, C => B)) => (c: C) => cone._1(c) -> cone._2(c),
  left  = [C] => (ulim: C => A × B) => (ulim andThen {_._1}) -> (ulim andThen {_._2})
)

Такой предел действительно минимален — морфизмы из других кандидатов в него будет единственным образом забывать любую лишнюю информацию. А будучи полиморфными, проекторы предела будут гарантировано факторизовать проекторы всех других кандидатов (сохранять коммутативность диаграммы). Значит, это и есть настоящее универсальное свойство предела для функтора, ответственного за выбор пары типов в категории \star.

Вообще, (ко)предел определяется не для категории индексов \mathcal{I}, а именно для функтора \mathcal{I} \rightarrow \mathcal{C}. Функторы бывают разные, в том числе, и \Delta_c схлопывающий все морфизмы (не только тождественные!) в один. Однако, часто предел ищется только для наиболее «свободного» функтора, моделирующего категорию \mathcal{I} в \mathcal{C} без потерь. В этом случае, (ко)предел определяется только структурой \mathcal{I}. Например, очевидно, что если в дискретной \mathcal{I} будет n объектов, то пределом функтора из неё в типы будет кортеж из n элементов.

Формально, можно вывести несколько разных вариантов одного и того же предела функтора. Например, произведение типов можно описать с помощью кодирования Чёрча как [A, B] =>> [C] => (A => B => C) => C. Но для всех этих вариантов в левой части естественный изоморфизма будут всё те же конусы, а значит и правые части будут изоморфны для всех вариантов. Таким образом, предел функтора единственен с точностью до изоморфизма (если он вообще существует).

Аналогично, убеждаемся, что копределом бифунктора F_{ab} будет сумма типов:

type Cocone2[A, B] = [+C] =>> ( // C - кандидат
  A => C, // первая компонента естественного преобразования
  B => C  // вторая компонента естественного преобразования
)
type UColim[A, B] = [+C] =>> Colim[A, B] => C

type Colim[A, B] = A + B // A Either B

given colimIso: [A, B] => Cocone2[A, B] ≅ UColim[A, B] = (
  right = [C] => (cocone: (A => C, B => C)) => (colim: Colim[A, B]) =>
    colim.fold(cocone._1, cocone._2),
  left  = [C] => (ucolim: (A + B) => C) =>
    ((a: A) => ucolim(Left(a)), (b: B) => ucolim(Right(b)))

В случае бо́льших размеров категории \mathcal{I} наиболее близкой реализация копредела в Scala будет, пожалуй, перечисление подобного вида:

enum Sum3[A, B, C]: // три конструктора:
  case One(a: A)
  case Two(b: B)
  case Tri(c: C)

Для экспоненциала в категории типов (то есть, функций A => B) также существует конструкция, которую можно интерпретировать как предел константного функтора \Delta\, B. Но теперь категория индексов \mathcal{I} будет дискретной категории значений типа A. Компоненты конусов кандидатов будут иметь вид:

// типы A и B фиксированы
// компонетны конуса индексированы значениям A
type ConeDelta = [-C] =>> A => (C => B)
type ULimExp   = [-C] =>> C => LimExp

type LimExp = A => B

given expIso: ConeDelta ≅ ULimExp = (
  right = [C] => (cone: A => C => B) =>
    (c: C) => (a: A) => cone(a)(c)
  left  = [C] => (ulim: C => A => B) =>
    (a: A) => (c: C) => ulim(c)(a)
)

Изоморфизм функторов отражает простое алгебраическое соотношение (B^C)^A = (B^A)^C, его морфизмы суть просто частичное применение каррированных функций.

Итак, мы можем строить новые типы складывая, умножая и «возводя в степень» существующие. А как определяются универсальные свойства нулевого и единичного типов, «существующих изначально»? Да совершенно аналогичным способом! Только если раньше мы строили новые типы из двух, моделируемых категорией \mathcal{I}=2, теперь нам не требуется ни одного типа, что соответствует абсолютно пустой категории \mathcal{I}=0. Единственно возможный функтор честно отображает пустую категорию в пустую диаграмму в \star, и от (ко)конусов остаются одни лишь вершины-кандидаты. В случае предела полученного функтора все универсальные морфизмы будут вести к нему, в то время как для копредела они будут вести от него к кандидатам. Это в точности соответствует универсальным свойствам терминального и начального объектов в категории типов — единичного (Unit) и нулевого (Nothing) типов!

Помимо вышеперечисленных, иногда говорят и о других (ко)пределах функторов, основанных на недискретных индексных категориях, то есть, имеющих морфизмы между объектами. В частности,

Однако «честные» реализации соответствующих типов не возможны без привлечения специальных техник Scala, основанных на идее зависимых типов, поэтому мы не будем останавливаться на них в данном обзоре.

Непрерывность и когерентность функторов

Пределы и копределы очень важны для большинства категорий, поэтому часто возникает вопрос о том, как с ними взаимодействуют функторы. Если функтор F сохраняет пределы любых других функторов G, он называется непрерывным:

{\rm Lim}\,(F \circ G) = (F \circ {\rm Lim})\, G.

(Напомню, предел — это функтор, определённый на категории функторов. То есть, его можно как композировать с другими функторами, так и применять к ним.)

Пределы являются функторами из \mathcal{C}^{\mathcal{I}} в \mathcal{C}, так что при фиксированной категории \mathcal{C} пределы будут отличаться для разных \mathcal{I}. Самый «базовый» предел для категории типов — это произведение типов. Поэтому непрерывность функтора обычно проверяют выясняя, сохраняет ли он произведение. Но в более общем случае для гарантии непрерывности требуется проверка сохранения и других пределов.

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

Среди эндофункторов в категории типов непрерывно-когерентным будет только тождественный функтор.

Сопряжения функторов

Пределы и копределы формулируются для конкретных функторов, вроде рассмотренного выше F_{a, b}:\: 2 \rightarrow \mathcal{C}. Каждый такой функтор выбирает в конечной категории, в каком именно месте будет смоделирована исходная. Между тем, натуральные изоморфизмы, определяющие предел и копредел намекают, что существуют глобальные взаимоотношения, не завязанные на выбор конкретного функтора F_{a, b}. Эти взаимоотношения и станут ключом к пониманию основных операций над типами — суммой, произведением и экспоненциалом.

Рассмотрим внимательнее представленный ранее натуральный изоморфизм, определяющий предел. Он индексирован объектом c:\: \mathcal{C} и функтором F:\: \mathcal{C}^\mathcal{I}, и представлен парой естественных преобразований, действующих навстречу друг другу:

\begin{split}left(c,F)&:&\: (\Delta c \rightsquigarrow F) &\rightarrow (c \rightarrow \mathrm{Lim}\,F),\\right(c,F)&:&\: (c \rightarrow \mathrm{Lim}\,F) &\rightarrow (\Delta c \rightsquigarrow F).\end{split}

В целях обобщения, попробуем реорганизовать эти конструкции, избавившись от упоминания c и F. Сперва для наглядности перепишем их через hom-объекты:

\begin{split}left(c,F)&:\: \mathrm{Hom}_{\mathcal{C}^\mathcal{I}}&&(\Delta& c,\,& &F) &\rightarrow \mathrm{Hom}_{\mathcal{C}}&&(&c,\;& \mathrm{Lim}\,&F),\\right(c,F)&:\: \mathrm{Hom}_{\mathcal{C}}&&(&c,\;& \mathrm{Lim}\, &F) &\rightarrow \mathrm{Hom}_{\mathcal{C}^\mathcal{I}}&&(\Delta &c,\,& &F).\end{split}

Тут лучше виден паттерн в расположении c и F относительно функторов \Delta:\: \mathcal{C} \rightarrow \mathcal{C}^\mathcal{I} и \mathrm{Lim}:\: \mathcal{C}^\mathcal{I} \rightarrow \mathcal{C} . В первом выражении давайте избавимся от F, подставив вместо него \Delta c, тогда слева от стрелки получится \mathrm{Hom}_{\mathcal{C}^\mathcal{I}}(\Delta c,\, \Delta c). Можем передать сюда тождественный функтор id_{\mathcal{C}^\mathcal{I}}, при этом останется лишь то, что правее стрелки — \mathrm{Hom}_{\mathcal{C}}(c,\; \mathrm{Lim}\,\Delta c). Перепишем это так:

left(c, \Delta c)(id_{\mathcal{C}^\mathcal{I}}):\: c \rightarrow (\mathrm{Lim} \circ \Delta)c.

Аналогично, если в правую ветку вместо c подставить \mathrm{Lim}\,F, и передать туда id_{\mathcal{C}}, получим

right(\mathrm{Lim}\,F, F)(id_{\mathcal{C}}):\: (\Delta \circ \mathrm{Lim})F \rightarrow F.

Оба выражения оказались индексированы только одним параметром и, очевидно, что они представляют собой компоненты естественных преобразований:

\begin{split} \eta_{\mathrm{Lim},\Delta}&:\:& id_{\mathcal{C}} \rightsquigarrow \mathrm{Lim} \circ \Delta &= \forall c: \mathcal{C}&& .\; left(c, &&\Delta c&&)(id_{\mathcal{C}^\mathcal{I}})\\  \varepsilon_{\mathrm{Lim},\,\Delta}&:\:& \Delta \circ \mathrm{Lim} \rightsquigarrow id_{\mathcal{C}^\mathcal{I}} &= \forall F: \mathcal{C}^\mathcal{I}&& .\; right(\mathrm{Lim}\,F, && \,\,\,F &&)(id_{\mathcal{C}}) \end{split}

Подобные преобразования могут существовать не только для \mathrm{Lim} и \Delta но и для других встречных функторов. Они определяют такое понятие, как сопряжение функторов:

\begin{split}F \dashv G&:&\\\eta&:&\: id \rightsquigarrow G \circ F,\\\varepsilon&:&\: F \circ G \rightsquigarrow id.\\\end{split}

Преобразования \eta называется «единицей» сопряжения. Его можно визуализировать так:

Крансой волнистой стрелкой обозначена единица сопряжения , а тонкими красными стрелками — пара его компонент.
Крансой волнистой стрелкой обозначена единица сопряжения \eta, а тонкими красными стрелками — пара его компонент.

Дуально, преобразование \varepsilon называются «коединицей»:

Крансой волнистой стрелкой обозначена коединица сопряжения , а тонкими красными стрелками — пара его компонент.
Крансой волнистой стрелкой обозначена коединица сопряжения \varepsilon, а тонкими красными стрелками — пара его компонент.

Между ними существуют очевидные условия консистентности — приведённые ниже треугольники должны коммутировать:

Правила треугольников для сопряжённых функторов.
Правила треугольников для сопряжённых функторов.
Если , то морфизмы  взаимно однозначно соответствуют морфизмам .
Если F \dashv G, то морфизмы F\, a \rightarrow b взаимно однозначно соответствуют морфизмам a \rightarrow G\, b.

Признаком наличия сопряжения между какими-то функторами как раз и является наличие естественного изоморфизма между hom-объектами в разных категориях.

Очень важный момент — сопряжение функторов не является отношением порядка. Оно не транзитивно, то есть, если F \dashv G и G \dashv H, то вовсе не обязательно, что F \dashv H. Сопряжение не является свойством одного функтора по отношению к другим, ведь левое и правое сопряжения любого функтора единственны с точностью до изоморфизма (если они вообще существуют).

Зато его можно интерпретировать как «ослабленную эквивалентность категорий» для случаев, когда строгая эквивалентность не нужна. Действительно, категории \mathcal{C} и \mathcal{D} считались бы эквивалентными, если бы для встречных функторов F и G между ними существовали бы натуральные изоморфизмы: F \circ G \cong id_{\mathcal{D}} и G \circ F \cong id_{\mathcal{C}}. А в случае сопряжений не нужны полные изоморфизмы, состоящие из четырёх естественных преобразований — достаточно лишь двух из них: id_{\mathcal{D}} \rightsquigarrow G \circ F и F \circ G \rightsquigarrow id_{\mathcal{C}}.

Монады и комонады

В примере с диагональным функтором и пределом приходилось работать не только с привычной категорией типов \mathcal{C}, но и с «неудобной» категорией функторов \mathcal{C}^\mathcal{I}. И также получается со многими сопряжениями — даже если какая-та категория нам и интересна, то с другой может и не повезти. Тогда какая может быть практическая польза от этих сопряжений?

Дело в том, что наибольший интерес представляют не сами функторы, участвующие в сопряжении F \dashv G, но их композиций F \circ G и G \circ F. Во-первых, обе этих композиции являются эндофункторами в своих категориях. А во-вторых, естественные преобразования сопряжения автоматически формируют для этих эндофункторов монаду и комонаду соответственно!

Действительно, «запаковка» G \circ F и «распаковка» F \circ G совпадают с единицей \eta и коединицей \varepsilon сопряжения, а «разматрёшивание» G \circ F и «заматрёшивание» F \circ G выражаются так:

\begin{eqnarray}\mu&:\;&&& (G \circ F) \circ (G \circ F) &\rightsquigarrow&& G \circ F &= id_G \circ \varepsilon \circ id_F,\\\delta&:\;&&& F \circ G &\rightsquigarrow&& (F \circ G) \circ (F \circ G) &= id_F \circ \eta \circ id_G.\end{eqnarray}

Таким образом, сопряжение функторов порождает монаду и комонаду. Но верно и обратное — любую (ко)монаду можно разложить на пару сопряжённых функторов. Только такое разложение будет неоднозначным, ведь заранее даже не известно, через какую дополнительную категорию будет факторизоваться эндофунктор.

Тут очень хорошо просматривается дуализм монады и комонады. Если первая строится для композиции F \circ G сопряжения F \dashv G, то вторая получается автоматически, но уже для перестановки G \circ F. В предыдущей части обзора мы уже видели похожую картину. Тогда для эндофункторов, соответствующих Writer[L] = [X] =>> L × X и Reader[R] = [X] =>> R => X, одна их композция давала монаду State[S] = Reader[S] ∘ Writer[S], а другая — комонаду Store[S] = Writer[S] ∘ Reader[S]. Можно ожидать, что Writerи Reader образуют сопряжение, и так оно и есть — мы убедимся в этом далее.

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

Сопряжённая тройка

Ранее мы видели, что для каждого функтора существует лишь единственный предел (для простоты не будем больше добавлять «с точностью до изоморфизма»). Это правило завязано на изоморфизме hom-типов, поэтому оно также распространяется и на сопряжения — для любого функтора может быть лишь один сопряженный слева и лишь один сопряжённый справа. Многие функторы, связанные с категорией типов имеют оба сопряжения.

Характерным примером является диагональный функтор \Delta:\; \mathcal{C} \rightarrow \mathcal{C}^\mathcal{I}. Для декартово-замкнутых категорий \mathcal{C} (в т.ч. для категории типов) правым и левым сопряжением диагонального функтора будут предел и копредел соответственно. Это удобно представить в виде сопряжённой тройки:

Выберем некий G: \mathcal{C} \rightarrow \mathcal{D}. Построим вокруг него сопряжённую тройку F \dashv G \dashv H с функторами F, H: \mathcal{D} \rightarrow \mathcal{C}. Это даст нам две пары эндофункторов для каждой категории:

\begin{eqnarray}H \circ G,\, F \circ G&:\;& \mathcal{C} &\rightarrow& \mathcal{C},\\G \circ F,\, G \circ H&:\;& \mathcal{D} &\rightarrow& \mathcal{D}.\\\end{eqnarray}

Причём, в каждой паре для первого эндофунктора сопряжения дадут монаду, а для второго — комонаду.

Более того, каждая пара образует новое сопряжение:

\begin{eqnarray}F \circ G \dashv H \circ G,\\G \circ F \dashv G \circ H.\\\end{eqnarray}

Саму возможность их получения проще увидеть, рассматривая сопряжения как изоморфизмы hom-типов:

\begin{eqnarray}\mathrm{Hom}(&&&F \circ G\, a&,\;&& b&)                  \hspace{1cm}  &&\mathrm{Hom}(&&&G \circ F\, a&,\;&& b&)   \\\Updownarrow {\tiny F \dashv G}\hspace{-2mm} &&&&&&&&&     \hspace{1cm}  \Updownarrow {\tiny G \dashv H} \hspace{-2mm} \\\mathrm{Hom}(&&&G\, a&,\;&& G\, b&)                      \hspace{1cm}  &&\mathrm{Hom}(&&&F\, a&,\;&& H\, b&)       \\\Updownarrow {\tiny G \dashv H}\hspace{-2mm} &&&&&&&&&     \hspace{1cm}  \Updownarrow {\tiny F \dashv G} \hspace{-2mm} \\\mathrm{Hom}(&&&a&,\;&& H \circ G\, b&)                  \hspace{1cm}  &&\mathrm{Hom}(&&&a&,\;&& G \circ H\, b&)   \\\end{eqnarray}

Из этой схемы можно вывести интересное следствие — алгебра монады F \circ G\ a \rightarrow a изоморфна коалгебре комонады a \rightarrow H \circ G\, a. То есть, буквально, если у вас есть «законопослушная распаковка» для F \circ G, то вам автоматически доступна столь же хорошая «запаковка» H \circ G, и наоборот. Аналогичное правило получается и для комонады G \circ F с монадой G \circ H.

Но нам важнее конструктивный вывод новых преобразований из первоначальных. Например, для сопряжения F \circ G \dashv H \circ G получаем:

\left\{\begin{eqnarray}\eta_{\tiny F \circ G \dashv H \circ G}: Id_{\mathcal{C}} \rightsquigarrow H \circ G \circ F \circ G =(Id_H \circ \eta_{\tiny F \dashv G} \circ Id_G) \cdot \eta_{\tiny G \dashv H}\\\varepsilon_{\tiny F \circ G \dashv H \circ G}: F \circ G \circ H \circ G \rightsquigarrow Id_{\mathcal{C}} =\varepsilon_{\tiny F \dashv G} \cdot (Id_F \circ \varepsilon_{\tiny G \dashv H} \circ Id_G)\end{eqnarray}\right.

Аналогично, для G \circ F \dashv G \circ H:

\left\{\begin{eqnarray}\eta_{\tiny G \circ F \dashv G \circ H}: Id_{\mathcal{D}} \rightsquigarrow G \circ H \circ G \circ F =(Id_G \circ \eta_{\tiny G \dashv H} \circ Id_F) \cdot \eta_{\tiny F \dashv G}\\\varepsilon_{\tiny G \circ F \dashv G \circ H}: G \circ F \circ G \circ H \rightsquigarrow Id_{\mathcal{D}} =\varepsilon_{\tiny G \dashv H} \cdot (Id_G \circ \varepsilon_{\tiny F \dashv G} \circ Id_H)\end{eqnarray}\right.

Здесь Id_{\mathcal{C}},\, Id_{\mathcal{D}} — тождественные функторы в соответствующих категориях, а Id_F,\, Id_G,\, Id_H — тождественные естественные преобразования функторов. Напомню, горизонтальная композиция \beta = Id_F \circ \alpha представляет собой естественной преобразование, в котором компоненты \beta получаются «подъёмом» исходных: \beta_a = F\, \alpha_a.

Более того, из выведенных только что сопряжений мы сразу получаем ещё две пары монад/комонад для композиций уже из четырёх функторов! В результате выстраивается такая цепочка:

  • сперва мы имеем единственный функтор G: \mathcal{C} \rightarrow \mathcal{D};

  • мы можем найти левый и правый сопряженный ему F, H: \mathcal{D} \rightarrow \mathcal{C};

  • таким образом, получим сопряжённую тройку F \dashv G \dashv H;

  • а из сопряжённой тройки автоматически получаются:

    • монады для эндофункторов

      • H \circ G,\;\; H \circ G \circ F \circ G:\;\; \mathcal{C} \rightarrow \mathcal{C};

      • G \circ F,\;\; G \circ F \circ G \circ H:\;\; \mathcal{D} \rightarrow \mathcal{D};

    • комонады для эндофункторов

      • F \circ G,\;\; F \circ G \circ H \circ G:\;\; \mathcal{C} \rightarrow \mathcal{C};

      • G \circ H,\;\; G \circ H \circ G \circ F:\;\; \mathcal{D} \rightarrow \mathcal{D};

  • в дополнение, имеем изоморфизмы алгебр и коалгебр, которые тоже могу оказаться полезными.

Итак, из самого наличия функтора (почти) автоматически получается целая куча связанных с ним монад и комонад! В общем случае они группируются в пары по категориям, но если мы сразу выбираем эндофунктор G:\; \mathcal{C} \rightarrow \mathcal{C}, то одних только монад в той же категории \mathcal{C} мы получим аж четыре штуки. Но и это ещё не всё! Теоретически, можно поискать как более правые, так и более левые сопряжения, расширяя сопряжённую тройку в обе стороны, получая всё новые эндофункторы и (ко)монады.

На текущий момент, самым неочевидным пунктом является возможность конструктивного вывода левого и правого сопряжений функтора. Этому будет посвящена следующая часть обзора. А сейчас рассмотрим некоторые следствия, важные для программистов.

Сопряжения на Scala

Ранее мы видели, что правое и левое сопряжение диагонального функтора являются пределом и копределом, что в категории типов соответствует произведению и сумме типов. Более конкретно, если выберем в качестве индексирующей дискретную категорию двух элементов \mathcal{I}=2, то получаем красивую сопряжённую тройку:

+ \dashv \Delta \dashv \times

Здесь +,\, \times:\; \star\times\star \rightarrow \star представляют собой знакомые бифункторы в категории типов:

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

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

Для эндофункторов в категории типов + \circ \Delta и \times \circ \Delta мы получим не особо интересные монады — они соответствуют редко используемым конструкторам типов [A] => A Either A и [A] => (A, A). Но можно попробовать декомпозировать на сопряжённые следующие эндофункторы:

type Result[E] = [A] =>> E + A
type Writer[L] = [A] =>> L × A

Впрочем, это не так-то просто сделать. Сопряжённые функторы для Result проходят через нетривиальную категорию косреза (coslice category), а в случае Writer промежуточная категория оказывается ещё сложнее (категория правых модулей моноида). И в обоих случаях сопряжённые функторы можно выразить лишь посредством зависимых типов. По указанным причинам не стану углубляться в эту тему, а выделю лишь главную идею:

несмотря на сложность промежуточных абстракций, сами они являются прямым следствием начальных условий задачи декомпозиции данных эндофункторов на сопряжённые, а значит, реализации их монадных возможностей являются не просто интуитивной догадкой — они выводятся непосредственно из тех же самых условий!

На практике программисты могут встретиться с сопряжениями эндофункторов в категории типов. Давайте сперва посмотрим, как сопряжение представлено в программировании:

@targetName("Adjunction")
infix type ⊣[F[_], G[_]] = (
  η: Id ~> (G ∘ F),
  ε: (F ∘ G) ~> Id
)

inline def   unitAdj[F[_], G[_]](using adj: F ⊣ G) = adj.η
inline def counitAdj[F[_], G[_]](using adj: F ⊣ G) = adj.ε

Альтернативный взгляд на сопряжение даёт изоморфизм hom-функторов. В категории типов эти hom-фунторы представляют собой обычные типы функций, а изоморфизм записывается так:

infix type AdjHomIso[F[_], G[_]] = (
  left : [A, B] => (F[A] =>   B ) => (  A  => G[B]),
  right: [A, B] => (  A  => G[B]) => (F[A] =>   B ),
)

inline def  leftAdj[F[_], G[_]](using adjIso: AdjHomIso[F, G]) = adjIso.left
inline def rightAdj[F[_], G[_]](using adjIso: AdjHomIso[F, G]) = adjIso.right

Оба представления изоморфны, это можно продемонстрировать, предоставив такие встречные преобразования:

given [F[_], G[_]] => AdjHomIso[F, G] => F ⊣ G = (
  η = [A] => (a: A)         =>  leftAdj[F, G][A, F[A]](identity)(a),
  ε = [B] => (fgb: F[G[B]]) => rightAdj[F, G][G[B], B](identity)(fgb),
)

given [F[_]: Functor, G[_]: Functor] => F ⊣ G => AdjHomIso[F, G] = (
  left  = [A, B] => (fab: F[A] =>   B ) =>   unitAdj[F, G][A] andThen fmap[G](fab),
  right = [A, B] => (agb:   A  => G[B]) => counitAdj[F, G][B] compose fmap[F](agb),
)

Обратите внимание, для получение hom-изоморфизма из сопряжения уже явно требуются функториальные возможности F[_] и G[_], задействованные в методе def fmap[F[_]: Functor as lift] = lift.

Но самое, важное, из-за чего мы разбираем сопряжения, это возможность получения из них (ко)монад. Используя определения классов типов из предыдущей части обзора, можно записать так:

inline def id[F[_]]: F ~> F = [A] => (fa: F[A]) => fa // тождественное естественное преобразование

given [F[_]: Functor, G[_]: Functor] => F ⊣ G => Monad[G ∘ F] = (
  lift    = summon[Functor[G ∘ F]],        // призывается compFunctor
  pure    = unitAdj[F, G],                 // Id            ~> G ∘ F
  flatten = id[G] ∘ counitAdj[F, G] ∘ id[F] // G ∘ F ∘ G ∘ F ~> G ∘ F
)

given [F[_]: Functor, G[_]: Functor] => F ⊣ G => Comonad[F ∘ G] = (
  lift      = summon[Functor[F ∘ G]],       // призывается compFunctor
  extract   = counitAdj[F, G],              // F ∘ G ~> Id
  coflatten = id[F] ∘ unitAdj[F, G] ∘ id[G]  // F ∘ G ~> G ∘ F ∘ G ∘ F
)

Композиция функторов compFunctor определена ранее тут, а горизонтальная композиция естественных преобразований α ∘ βтут.

Для раскрытия возможностей сопряжённых троек помогут такие композиции сопряжений:

given adjComp1: [F[_]: Functor, G[_]: Functor, H[_]: Functor] => F ⊣ G => G ⊣ H => (F ∘ G) ⊣ (H ∘ G) = (
  η = (id[H] ∘ unitAdj[F, G] ∘ id[G]) ⋅ unitAdj[G, H],
  ε = counitAdj[F, G] ⋅ (id[F] ∘ counitAdj[G, H] ∘ id[G])
)

given adjComp2: [F[_]: Functor, G[_]: Functor, H[_]: Functor] => F ⊣ G => G ⊣ H => (G ∘ F) ⊣ (G ∘ H) = (
  η = (id[G] ∘ unitAdj[G, H] ∘ id[F]) ⋅ unitAdj[F, G],
  ε = counitAdj[G, H] ⋅ (id[G] ∘ counitAdj[F, G] ∘ id[H])
)

Вертикальная композиция естественных преобразований α ⋅ β вводилась тут.

Давайте посмотрим, как это работает.

Контейнеры состояния и хранилища

Самый типичный пример, упоминаемый по данной теме в популярных источниках, это демонстрация и анализ сопряжения Writer[S] ⊣ Reader[S]. Давайте и мы сейчас это разберём, благо, само сопряжение реализуется очень просто:

type Writer[L] = [A] =>> L  ×  A
type Reader[R] = [A] =>> R  => A

given [L] => Functor[Writer[L]] = [A, B] => (f: A => B) => (wla: Writer[L][A]) => wla._1 -> f(wla._2)
given [R] => Functor[Reader[R]] = [A, B] => (f: A => B) => (rra: Reader[R][A]) => rra andThen f

given [S] => Writer[S] ⊣ Reader[S] = (
  η = [A] => (a: A) => (s: S) => s -> a,
  ε = [A] => (ssa: (S, S => A)) => ssa._2(ssa._1)
)

Связанный с сопряжением изоморфизм hom-функторов отражает простое алгебраическое соотношение:

B^{(S \times A)} \cong (B^S)^A\\

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

Особый интерес представляют композиции этих функторов, для которых сопряжение даёт монаду и комонаду:

type State[S] = Reader[S] ∘ Writer[S] // S => (S, A)   монада
type Store[S] = Writer[S] ∘ Reader[S] // (S, S => A) комонада

Контейнер State[S] отвечает за извлечение значения A из неизвестного состояния s: S, при этом итоговое состояние может измениться. В свою очередь, Store[S] уже содержит некоторое состояние, и способ извлечения из него значения A. Значение a: A всегда можно запаковать в State[S][A], но не в Store[S][A], для которого требуется также состояние s: S. С другой стороны, у Store уже есть всё необходимое для распаковки A, в то время как для извлечения из State недостаёт s: S.

Оба композитных функтора имеют схожую семантику работы со значением A как малой части состояния S, но эти контейнеры, очевидно, не изоморфны. Cопряжение Writer[S] ⊣ Reader[S] даёт для них разные возможности — Monad[State] и Comonad[Store]. Собственно, различие между этими контейнерами как раз соответствует различным целям использования монад и комонад.

Контейнер State ориентирован на композицию вычислений с эффектом изменения состояния — «разматрёшивание» оставляет в итоге лишь конечное состояние и забывает все промежуточные. Вот несколько методов, характерных для этого контейнера:

object State:
  def get [S] = (s: S) => s -> s             // в контейнере сохраняется переданное состояние
  def put [S] = (s: S) => (_:S) => s -> ()   // создаётся контейнер с пустым значнием, замещающий состояние
  def pure[S] = summon[Monad[State[S]]].pure // запаковка

extension [S, A](st: State[S][A])
  def runS = st andThen {_._1}         // вычисляем итоговое состояние
  def runA = st andThen {_._2}         // вычисляем итоговое значение
  def run  = st                        // вычисляем значение и состояние
  def modify(f: S => S) = f andThen st // изменяем состояние
  def inspect[B](f: S => B) = (s: S) => s -> f(s) // значение из состояния

Вот пример использования State для подсчёта изменений:

val f1: Double => State[Int][Double] = d => c => c + 1 -> (d + c) // увличиваем счётчик
val f2: Double => State[Int][String] = d => _ -> ("строка: " + d) // состояние не меняем
  
val resultEffect = State
  .pure(5.0)   // «запаковываем» 5.0
  .flatMap(f1) // увличиваем счётчик 1й раз
  .flatMap(f1) // увличиваем счётчик 2й раз
  .flatMap(f1) // увличиваем счётчик 3й раз
  .flatMap(f2) // счётчик не меняется

//старт счётчика ↓      ↓ итоговое состояние счётчика
resultEffect.run(0) // (3,строка: 8.0)
//                        ↑↑↑↑↑↑↑↑↑↑↑ итоговое значение

Контейнер Store[P][A] содержит позицию P и функцию вычисления значения A на основе позиции. Функция как раз и представляет собой хранилище значений A, индексированных позицией, а сам контейнер — это курсор, который знает своё «местоположение в хранилище значений»:

object Store:
  def apply[P, A](pos: P, peek: P => A): Store[P][A] = pos -> peek

extension [P, A](st: Store[P][A])
  def pos : P = st._1                                 // позиция
  def peek: A = summon[Comonad[Store[P]]].extract(st) // значение в позиции
  def seek (p: P)     : Store[P][A] =   p    -> st._2 // сдвигаем позицию
  def seeks(f: P => P): Store[P][A] = f(pos) -> st._2 // аналогично

«Заматрёшивание» Store дублирует текущую позицию, что вместе с функториальностью позволяет накапливать изменяющиеся позиции во вложенных контейнерах. Но всё же чаще эти возможности используются совместно в методе coflatmap (в других библиотеках extend). В качестве примера использования Store часто приводят преобразование изображений:

val image = (i: Int, j: Int) => i + j // диагональный градиент
val initialStore = Store((0, 0), image.tupled)

def transform(store: Store[(Int, Int)][Int]): Int =
  val (i, j) = store.pos
  store.seek((i + j) % 8, (i * j) % 8).peek
//↑↑↑↑↑↑↑↑↑↑ используем другие значения из хранилища, но относительно текущей позиции

val finalStore: Store[(Int, Int)][Int] = initialStore.coflatMap(transform)

Метод transform имеет доступ ко всему изображению и к текущей позиции.

«Разматрёшивание» State[S] превращает два состояния в одно, что вместе с другими свойствами намекает на полезность возможностей моноида для S. В Store[P] хранимое значение однозначно представляет позиция P. Представимость контейнеров и моноиды для контекстов контейнеров характерны для любых сопряжённых функторов, и для построенных на их основе монад/комонад. В продолжении обзора мы рассмотрим эту особенность детальнее, так как она помогает строить композитные (ко)монады.

Дополнительная литература

Промежуточный итог

Тезисно:

  • любой новый тип определяется путём описания его универсального свойства (мы предварительно знакомы с этим понятием);

  • категориальное описание универсальных свойств выражается через понятия пределов и копределов функторов, причём, копредел даёт сумму типов, а предел — произведение (экспоненциал типов также можно представить как предел);

  • сами (ко)пределы демонстрируют частный случай сопряжённых функторов — копредел является левым, а предел правым сопряжениями одного и того же диагонального функтора, отражающего простую идею выбора чего-то одного на основе какого-то набора;

  • сопряжение диагонального функтора с (ко)пределом подчёркивает фундаментальность алгебраических операций над типами, позволяющих сформулировать новый тип из существующих;

  • каждая пара сопряжённых функторов порождает монаду и комонаду;

  • для заданного функтора его левое и правое сопряжения единственны с точностью до изоморфизма;

  • если найти левое и правое сопряжение заданного функтора, это даст нам сопряжённую тройку, что позволит получить ещё больше композитных функторов и их (ко)монад.

Концепция сопряженных функторов считается ключевой в теории категорий, и надеюсь, мне удалось хоть немного продемонстрировать это. Но на данный момент мы ещё не выяснили, как же вычислять сопряжения в программировании, как находить типы сопряжённых функторов.

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