Представим список натуральных чисел и список строк. Типы элементов различаются, но устройство списка и операции над его структурой остаются общими. Обозначим тип элемента как : конструктор принимает его и создаёт конкретный тип . Поэтому род — .
Любитель C++ справедливо заметит, что шаблоны и уже позволяют абстрагироваться от типа элемента, а итераторы и ranges — применять одни и те же алгоритмы к разным структурам, например к списку и дереву. Однако такие алгоритмы обобщаются по интерфейсу обхода готового объекта, а не по конструктору типа, сохраняющему форму контекста.
Haskell делает следующий шаг. И , и имеют род , поэтому класс типов может абстрагироваться над самим конструктором . Операция принимает функцию , преобразует значения внутри и возвращает , сохраняя форму контекста. Это полиморфизм не только по типу элемента, но и по конструктору контекста. В C++ его можно имитировать шаблонами и перегрузками, однако прямого стандартного аналога классов типов и единообразного полиморфизма высших родов в языке нет. Сам класс типов объявляется так:
class Functor f where
fmap :: (a -> b) -> f a -> f b -- применить функцию внутри контекста fВ этом объявлении — переменная не конкретного типа, а конструктора типа рода . Класс задаёт общую форму операции , а способ сохранения контекста определяет каждый экземпляр. Для списка и стандартные экземпляры устроены так:
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)Как видим, реализация зависит от конструктора контекста, но не от конкретного типа его элементов. Теперь рассмотрим число 2 в некотором контексте. Начнём с ключевого понятия — значения в контексте:
Контекст позволяет один раз определить правила работы с особыми случаями и затем применять обычные функции, не размазывая проверки по алгоритму. Более мощные абстракции, построенные поверх этой идеи, позволяют связывать вычисления в цепочки и задавать стратегии их выполнения, но сам гарантирует только преобразование через . Допустим, вычисление в JavaScript вернуло не значение, а . Тогда приходится писать ; если передать результат дальше, проверки начинают расползаться по всему алгоритму. В Haskell отсутствие представляют явно с помощью : экземпляр оставляет без изменений, а к значению внутри применяет функцию. Точно так же отображение по пустому списку возвращает пустой список. К обычному значению функцию можно применить напрямую; значение в контексте сначала пришлось бы разбирать вручную. берёт эту работу на себя:
fmap (+3) (Just 2) -- Just 5
fmap (+3) Nothing -- NothingУ есть инфиксный синоним: .
Экземпляры должны соблюдать два закона — тождество и композицию:
fmap id == id -- тождество
fmap (g . h) == fmap g . fmap h -- композицияЭти законы означают, что меняет только значения, но не форму контекста: не перестраивает список и не превращает в . Кроме того, последовательное отображение двух функций эквивалентно отображению их композиции.
Главные экземпляры:
-- 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"Функтор — отображение между категориями, сохраняющее и композицию. В принятой для Haskell модели функторы являются эндофункторами категории типов и функций, а законы в точности соответствуют аксиомам функтора.