Изучение расширенного семейства функторов, часть I

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

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

Невозможный функтор

Как вы, возможно, знаете, функция формирует функтор, когда мы фиксируем аргументы типа параметров. Обычно он называется Reader и может быть определен так:

Но что, если мы исправим возвращаемый тип и сделаем тип аргумента «a» нашего функтора? Мы можем довольно легко определить тип, и он очень похож на любой из функторов с двумя параметрами типа, такими как Reader, Result или Either:

Это очень общее определение, имеющее множество конкретных примеров. Большинство из них - это функции, которые что-то говорят о «вещи», например, о предикатах, сортировке или равенстве. Некоторые примеры типов, которые следуют этому шаблону, показаны ниже:

Это все типы с одним аргументом типа, которые очень похожи на функторы. Но можем ли мы определить метод fmap? Ответ - нет. Вам нужно будет определить функцию от b до r, и у вас есть только две функции от a. Вы буквально ничего не можете сделать с имеющимися у вас функциями и ценностями.

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

Положительные и отрицательные дженерики

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

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

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

Определение контраварианта

Как мы видели, существует абстракция, подобная функтору, которая решает проблему наличия аргумента контравариантного типа. Этот функтор нельзя сопоставить традиционным способом с помощью функции от a до b, но мы можем сопоставить его с функцией от b до a. Эта функция называется contramap или cmap. Интерфейс для этого можно определить следующим образом:

С этого момента в этой серии я буду использовать fmap для ссылки на отображение ковариантного функтора, чтобы избежать путаницы с cmap и другими функциями отображения в будущем. Я могу показать вам, что эта абстракция работает, реализовав ее для некоторых примеров контравариантов, которые мы видели ранее в этой статье:

Контравариантные законы

Как и все интересное в функциональном программировании, у контраварианта есть некоторые законы, которым нужно подчиняться, прежде чем вы сможете назвать что-то контравариантом. Эти законы очень похожи на законы функторов, поскольку они основаны на той же логике. Первый - это закон идентичности, который точно такой же для cmap и fmap:

c.cmap(id) == id(c) == c

Этот закон гарантирует, что cmap отображает только элемент в функторе и ничего больше. Другой - закон композиции. Как и ковариантные функторы, контраварианты должны учитывать композицию при отображении. Основное отличие здесь в том, что f - это функция от b до a, а не функция от a до b. Аналогично, g - это функция от c до b, а не функция от b до c, как это было бы для закона композиции fmap.

c.cmap(f).cmap(g) == c.cmap(compose(f, g))

compose - это функция, составляющая g после f, определенная в TypeScript следующим образом:

Заключение

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