Статьи

Функторы JavaScript объяснены

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

fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B) 

Функция fmap принимает функцию (из A -> B) и функтор (в виде обернутого контекста) Wrapper (A) и возвращает новый функтор Wrapper (B), содержащий результат применения указанной функции к значению, а затем закрывает его еще раз. Вот быстрый пример использования функции приращения в качестве нашей функции отображения из A -> B (за исключением того, что в этом случае A и B имеют одинаковые типы): 

 

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

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

Теория функторов

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

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

Действительно, это немного теоретически, чтобы понять. Давайте рассмотрим очень простой пример. Рассмотрим простое сложение 2 + 3 = 5 с использованием функторов. Я могу использовать простую функцию add для создания функции plus3 как таковой:

var plus = R.curry((a, b) => a + b);  var plus3 = plus(3);

Теперь я запомню номер два в простой функтор Wrapper:

var two = wrap(2);

Вызов fmap для сопоставления plus3 над контейнером выполняет сложение: 

var five = two.fmap(plus3); //-> Wrapper(5)

 five.map(R.identity); //-> 5

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

two.fmap(plus3).fmap(plus10); //-> Wrapper(15)

Это сложно понять, поэтому вот визуальное представление о том, как fmap снова работает с plus3 на этом рисунке:

 

Рисунок 2 Значение 2 было добавлено в контейнер Wrapper. Функтор используется для манипулирования этим значением, сначала разворачивая его из контекста, применяя к нему заданную функцию и переупаковывая значение обратно в новый контекст.

Цель, чтобы fmap вернул тот же тип (или снова поместил результат в контейнер), состоит в том, чтобы мы могли продолжить операции цепочки. Рассмотрим следующий пример, который отображает плюс на упакованное значение и регистрирует результат, как показано в следующем коде:

var two = wrap(2);

two.fmap(plus3).fmap(R.tap(infoLogger)); //-> Wrapper(5)

Выполнение этого кода выводит на консоль следующее сообщение:

InfoLogger [INFO] 5

Звучит ли эта идея функций цепочки знакомой? На самом деле, вы использовали функторы все время, не осознавая этого. Это именно то, что функции map и filter делают для массивов:

map    :: (A -> B)   -> Array(A) -> Array(B)

filter :: (A -> Boolean) -> Array(A) -> Array(A)

Функции map и filter являются «гомоморфизмом между категориями». Причина в том, что обе функции сохраняют один и тот же тип:

  • гомо : то же самое 

  • морфизм : функция, которая поддерживает структуру

  • категория : тип значения содержится

Расширяя эту концепцию на функции, рассмотрим другой тип гомоморфного функтора, который вы видели все это время: compose. Как вы, возможно, знаете, функция compose — это отображение функций из других функций:

compose :: (B -> C) -> (A -> B) -> (A -> C)

Функторы, как и любой другой артефакт функционального программирования, регулируются некоторыми важными свойствами:

Они должны быть свободны от побочных эффектов: отображение функции R.identity может использоваться для получения того же значения в контексте. Это доказывает, что они не имеют побочных эффектов и сохраняют структуру упакованного значения. 

wrap('Get Functional').fmap(R.identity); //-> Wrapper('Get Functional')

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

two.fmap(R.compose(plus3, R.tap(infoLogger))).map(R.identity); //-> 5

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

Однако сами по себе функторы не слишком привлекательны и могут потерпеть неудачу при наличии нулевых данных, точно так же, как функтор карты массива, который эффективно пропускает нулевые элементы и создает, что пропускает вызов объекта нулевой функции. Это аналогично наличию пустого блока catch для игнорирования сбоя. На практике, однако, вам нужно будет правильно обрабатывать ошибки, и для этого вам потребуется новый функциональный тип данных, называемый Monads . Вы можете узнать больше о функторах и монадах в моей книге «Функциональное программирование на JavaScript».

Чтобы узнать больше о функциональном программировании, загрузите функциональное программирование DZone с помощью JavaScript Refcard от Luis Atencio .