Функторы

Представим список натуральных чисел и список строк. Типы элементов различаются, но устройство списка и операции над его структурой остаются общими. Обозначим тип элемента как T\mathtt{T}: конструктор List\mathtt{List} принимает его и создаёт конкретный тип List  T\mathtt{List\;T}. Поэтому род List\mathtt{List}\mathtt{* \to *}.

Любитель C++ справедливо заметит, что шаблоны std::vectorT\mathtt{std::vector}\langle \mathtt{T} \rangle и std::listT\mathtt{std::list}\langle \mathtt{T} \rangle уже позволяют абстрагироваться от типа элемента, а итераторы и ranges — применять одни и те же алгоритмы к разным структурам, например к списку и дереву. Однако такие алгоритмы обобщаются по интерфейсу обхода готового объекта, а не по конструктору типа, сохраняющему форму контекста.

Haskell делает следующий шаг. И List\mathtt{List}, и Tree\mathtt{Tree} имеют род \mathtt{* \to *}, поэтому класс типов Functor\mathtt{Functor} может абстрагироваться над самим конструктором f\mathtt{f}. Операция fmap\mathtt{fmap} принимает функцию ab\mathtt{a \to b}, преобразует значения внутри f  a\mathtt{f\;a} и возвращает f  b\mathtt{f\;b}, сохраняя форму контекста. Это полиморфизм не только по типу элемента, но и по конструктору контекста. В C++ его можно имитировать шаблонами и перегрузками, однако прямого стандартного аналога классов типов и единообразного полиморфизма высших родов в языке нет. Сам класс типов объявляется так:

class Functor f where
  fmap :: (a -> b) -> f a -> f b   -- применить функцию внутри контекста f

В этом объявлении f\mathtt{f} — переменная не конкретного типа, а конструктора типа рода \mathtt{* \to *}. Класс задаёт общую форму операции fmap\mathtt{fmap}, а способ сохранения контекста определяет каждый экземпляр. Для списка и Maybe\mathtt{Maybe} стандартные экземпляры устроены так:

instance Functor [] where
  fmap _ []       = []
  fmap g (x : xs) = g x : fmap g xs

instance Functor Maybe where
  fmap _ Nothing  = Nothing
  fmap g (Just x) = Just (g x)

Как видим, реализация Functor\mathtt{Functor} зависит от конструктора контекста, но не от конкретного типа его элементов. Теперь рассмотрим число 2 в некотором контексте. Начнём с ключевого понятия — значения в контексте:

Просто значение 2, значение в контексте Just 2 и пустой контекст Nothing
Рис. 2.3. Обычное значение и значение в контексте Maybe\mathtt{Maybe}: «коробка» может содержать значение (Just  2\mathtt{Just\;2}) или быть пустой (Nothing\mathtt{Nothing}).

Контекст позволяет один раз определить правила работы с особыми случаями и затем применять обычные функции, не размазывая проверки по алгоритму. Более мощные абстракции, построенные поверх этой идеи, позволяют связывать вычисления в цепочки и задавать стратегии их выполнения, но сам Functor\mathtt{Functor} гарантирует только преобразование через fmap\mathtt{fmap}. Допустим, вычисление в JavaScript вернуло не значение, а undefined\mathtt{undefined}. Тогда приходится писать if\mathtt{if}; если передать результат дальше, проверки начинают расползаться по всему алгоритму. В Haskell отсутствие представляют явно с помощью Maybe\mathtt{Maybe}: экземпляр Functor\mathtt{Functor} оставляет Nothing\mathtt{Nothing} без изменений, а к значению внутри Just\mathtt{Just} применяет функцию. Точно так же отображение по пустому списку возвращает пустой список. К обычному значению функцию можно применить напрямую; значение в контексте сначала пришлось бы разбирать вручную. fmap\mathtt{fmap} берёт эту работу на себя:

fmap применяет функцию внутри контекста: Just 2 превращается в Just 5, Nothing остаётся Nothing, список отображается поэлементно
Рис. 2.4. fmap\mathtt{fmap} применяет функцию к значению внутри контекста и сохраняет сам контекст: пустая коробка остаётся пустой, а список отображается поэлементно.
fmap (+3) (Just 2)   -- Just 5
fmap (+3) Nothing    -- Nothing

У fmap\mathtt{fmap} есть инфиксный синоним: g<$>xg \mathbin{\mathtt{<\$>}} x =fmap  g  x= \mathtt{fmap}\;g\;x.

Экземпляры должны соблюдать два закона — тождество и композицию:

fmap id == id                     -- тождество
fmap (g . h) == fmap g . fmap h   -- композиция

Эти законы означают, что fmap\mathtt{fmap} меняет только значения, но не форму контекста: не перестраивает список и не превращает Just\mathtt{Just} в Nothing\mathtt{Nothing}. Кроме того, последовательное отображение двух функций эквивалентно отображению их композиции.

Главные экземпляры:

-- Maybe — контекст «значение может отсутствовать»
fmap (+3) (Just 2)       -- Just 5

-- список — контекст «ноль или несколько значений»: fmap = map
fmap (+3) [1, 2, 3]      -- [4, 5, 6]

-- Either e — отображается только Right
fmap (+3) (Right 2)      -- Right 5
fmap (+3) (Left "oops")  -- Left "oops"

Функтор — отображение между категориями, сохраняющее id\mathtt{id} и композицию. В принятой для Haskell модели функторы являются эндофункторами категории типов и функций, а законы fmap\mathtt{fmap} в точности соответствуют аксиомам функтора.