Теперь вспомним о категории Hask. В этой категории объектами являются типы, а стрелками функции.
Функтор f отображает объекты и стрелки категории Hask в объекты и стрелки f Hask. При этом оказывается,
что за счёт свойств функтора f Hask образует категорию.
• Объекты – это типы f a.
• Стрелки – это функции fmap f.
• Композиция стрелок это просто композиция функций.
• Тождественная стрелка это fmap id.
Проверим аксиомы:
fmap f . fmap id = fmap f . id = fmap f
fmap id . fmap f = id . fmap f = fmap f
fmap f . (fmap g . fmap h)
=
fmap f . fmap (g . h)
=
fmap (f . (g . h))
=
fmap ((f . g) . h)
=
fmap (f . g) . fmap h
=
(fmap f . fmap g) . fmap h
Функтор | 229
Видно, что аксиомы выполнены, так функтор f порождает категорию f Hask. Интересно, что поскольку
Hask содержит все типы, то она содержит и типы f Hask. Получается, что мы построили категорию внутри
категории. Это можно пояснить на примере списков. Тип [] погружает любой тип в список, а функцию для
любого типа можно превратить в функцию, которая работает на списках с помощью метода fmap. При этом с
помощью класса Functor мы проецируем все типы и все функции в мир списков [a]. Но сам этот мир списков
содержится в категории Hask.
С помощью функторов мы строим внутри одной категории другую категорию, при этом внутренняя ка-
тегория обладает некоторой структурой. Так если раньше у нас были только произвольные типы a и произ-
вольные функции a -> b, то теперь все объекты имеют тип [a] и все функции имеют тип [a] -> [b]. Также и
функтор Maybe переводит произвольное значение, в значение, которое обладает определённой структурой. В
нём выделен дополнительный элемент Nothing, который обозначает отсутствие значения. Если по типу val
:: a мы ничего не можем сказать о содержании значения val, то по типу val :: Maybe a, мы знаем один
уровень конструкторов. Например мы уже можем проводить сопоставление с образцом.
Теперь давайте вернёмся к теории категорий и дадим формальное определение понятия. Пусть A и B –
категории, тогда функтором из A в B называют отображение F , которое переводит объекты A в объекты B
и стрелки A в стрелки B, так что выполнены следующие свойства:
F f
:
F A >B F B если f : A >A B
F idA
=
idF A
для любого объекта A из A
F ( f ; g)
=
F f ; F g
если ( f ; g) подходят по типам
Здесь запись >A и >B означает, что эти стрелки в разных категориях. После отображения стрелки f
из категории A мы получаем стрелку в категории B, это и отражено в типе F f : F A >B F B. Первое
свойство говорит о том, что после отображения стрелки соединяют те же объекты, что и до отображения.
Второе свойства говорит о сохранении тождественных стрелок. А последнее свойство, говорит о том, что
“пути” между объектами также сохраняются. Если мы находимся в категории A в объекте A и перед нами
есть путь состоящий из нескольких стрелок в объект B, то неважно как мы пойдём в F B либо мы пройдём
этот путь в категории A и в самом конце переместимся в F B или мы сначала переместимся в F A и затем
пройдём по образу пути в категории F B. Так и так мы попадём в одно и то же место. Схематически это
можно изобразить так:
f
g
A
B
C
F
F
F
F A
F B
F C
F f
F g
Стрелки сверху находятся в категории A, а стрелки снизу находятся в категории B. Функтор F : A > A,
который переводит категорию A в себя называют эндофунктором (endofunctor). Функторы отображают одни
категории в другие сохраняя структуру первой категории. Мы словно говорим, что внутри второй категории
есть структура подобная первой. Интересно, что последовательное применение функторов, также является
функтором. Мы будем писать последовательное применение функторов F и G слитно, как F G. Также можно
определить и тождественный функтор, который ничего не делает с категорией, мы будем обозначать его как
IA или просто I, если категория на которой он определён понятна из контекста. Это говорит о том, что мы
можем построить категорию, в которой объектами будут другие категории, а стрелками будут функторы.
15.3 Естественное преобразование
В программировании часто приходится переводить данные из одной структуры в другую. Каждая из
структур хранит какие-то конкретные значения, но мы ничего с ними не делаем мы просто перекладываем
содержимое из одного ящика в другой. Например в нашем ящике только один отсек, но вдруг нам пришло
бесконечно много подарков, что поделать нам приходится сохранить первый попавшийся, отбросив осталь-
ные. Главное в этой аналогии это то, что мы ничего не меняем, а лишь перекладываем содержимое из одной
структуры в другую.
В Haskell это можно описать так:
onlyOne :: [a] -> Maybe a
onlyOne []
= Nothing
onlyOne (a:as)
= Just a
В этой функции мы перекладываем элементы из списка [a] в частично определённое значение Maybe.
Тоже самое происходит и в функции concat:
230 | Глава 15: Теория категорий
concat :: [[a]] -> [a]
Элементы перекладываются из списка списков в один список. В теории категорий этот процесс называ-
ется естественным преобразованием. Структуры определяются функторами. Поэтому в определении будет
участвовать два функтора. В функции onlyOne это были функторы [] и Maybe. При перекладывании элемен-