Функциональное программирование с примерами на JavaScript. Часть первая. Основные техники функционального программирования

Рассказывает rajaraodv


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

В первой части вы изучите основы ФП, такие как каррирование, чистые функции, fantasy-land, функторы, монады, Maybe-монады и Either-монады на нескольких примерах.

Функциональное программирование — это стиль написания программ через составление набора функций.

Основной принцип ФП — оборачивать практически все в функции, писать множество маленьких многоразовых функций, а затем просто вызывать их одну за другой, чтобы получить результат вроде (func1.func2.func3) или в композиционном стиле func1(func2(func3())).

Кроме этого, структура функций должна следовать некоторым правилам, описанным ниже.

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

  1. Как реализовать условия (if-else)? (Совет: используйте монаду Either);
  2. Как перехватить исключения типа Null Exception? (В этом может помочь монада Maybe);
  3. Как убедиться в том, что функция действительно «многоразовая» и может использоваться в любом месте? (Чистые функции);
  4. Как убедиться, что данные, которые мы передаем, не изменяются, чтобы мы могли бы использовать их где-то еще? (Чистые функции, иммутабельность);
  5. Если функция принимает несколько значений, но цепочка может передавать только одно значение, как мы можем сделать эту функцию частью цепочки? (Каррирование и функции высшего порядка).

Чтобы решить все эти проблемы, функциональные языки, вроде Haskell, предоставляют инструменты и решения из математики, такие как монады, функторы и т.д., из коробки.

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

Спецификация Fantasy-Land и библиотеки ФП

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

Fantasy-Land — одна из таких спецификаций, в которой описано, как должна действовать та или иная функция или класс в JS.

функциональное программирование

На рисунке выше показаны все спецификации и их зависимости. Спецификации — это, по существу, описания функционала, подобные интерфейсам в Java. С точки зрения JS вы можете думать о спецификациях, как о классах или функциях-конструкторах, которые реализовывают некоторые методы (map, of, chain), следуя спецификации.

Например, класс в JavaScript является функтором, если он реализует метод map. Метод map должен работать, следуя спецификации.

По аналогии, класс в JS является аппликативным функтором, если он реализует функции map и ap.

JS-класс — монада, если он реализует функции, требуемые функтором, аппликативным функтором, цепочкой и самой монадой.

Библиотеки, следующие спецификациям Fantasy-Land

Есть несколько библиотек, следующих спецификациям FL: monet.js, barely-functional, folktalejs, ramda-fantasyimmutable-ext, Fluture и т.д.

Какие же из них мне использовать?

Такие библиотеки, как lodash-fp и ramdajs, позволяют вам начать программировать в функциональном стиле. Но они не реализуют функции, позволяющие использовать ключевые математические концепты (монады, функторы, свертки), а без них невозможно решать некоторые из реальных задач в функциональном стиле.

Так что в дополнение к ним вы должны использовать одну из библиотек, следующих спецификациям FL.

Теперь, когда мы знаем основы, давайте посмотрим на несколько практических примеров и изучим на них некоторые из возможностей и техник функционального программирования.

Пример 1: справляемся с проверкой на NULL

Тема покрывает: функторы, монады, Maybe-монады и каррирование.

Сценарий использования: Мы хотим показать различные стартовые страницы в зависимости от языка, выбранного пользователем в настройках. В данном примере мы реализовываем функцию getUrlForUser, которая возвращает правильный URL из списка indexURLs для испанского языка, выбранного пользователем joeUser.

Проблема: язык может быть не выбран, то есть равняться null. Также сам пользователь может быть не залогинен и равняться null. Выбранный язык может быть не доступен в нашем списке indexURLs. Так что мы должны позаботиться о нескольких случаях, при которых значение null или undefined может вызвать ошибку.

Решение (Императивное против Функционального):

Не беспокойтесь, если функциональное решение пока вам не понятно, я объясню его шаг за шагом немного позже.

Давайте для начала попробуем понять некоторые из концептов ФП, которые были использованы в этом решении.

Функторы

Любой класс или тип данных, который хранит значение и реализует метод map, называется функтором.

Например, Array — это функтор, потому что массив хранит значения и реализует метод map, позволяющий нам применять функцию к значениям, которые он хранит.

Давайте напишем свой собственный функтор «MyFunctor». Это просто JS-класс (функция-конструктор). Метод map применяет функцию к хранимым значениям и возвращает новый экземпляр MyFunctor.

Функторы так же должны реализовывать и другие спецификации в дополнение к методу map, но я не буду рассказывать о них в этой статье.

Монады

Монады — это подтип функторов, так как у них есть метод map, но они также реализуют другие методы, например, ap, of, chain.

Ниже представлена простая реализация монады.

Обычные монады используются нечасто, в отличие от более специфичных монад, таких как «монада Maybe» и «монада Either«.

«Maybe»-монада

Монада «Maybe» — это класс, который имплементирует спецификацию монады. Её особенность заключается в том, что с помощью нее можно решать проблемы с null и undefined.

В частности, в случае, если данные равны null или undefined, функция map пропускает их.

Код, представленный ниже, показывает имплементацию Maybe-монады в библиотеке ramda-fantasy. Она возвращает экземпляр одного из двух подклассов: Just или Nothing, в зависимости от значения.

Классы Just и Nothing содержат одинаковые методы (map, orElse и т.д.). Отличие между ними в реализации этих самых методов.

Обратите особое внимание на функции «map» и «orElse».

Давайте поймем, как Maybe-монада осуществляет проверку на null.

  1. Если есть объект, который может равняться null или иметь нулевые свойства, создаем экземпляр монады из него.
  2. Используем библиотеки, вроде ramdajs, чтобы получить значение из монады и работать с ним.
  3. Возвращаем значение по умолчанию, если данные равняются null.

Каррирование

Освещенные темы: чистые функции и композиция.

Если мы хотим создавать серии вызовов функций, как то func1.func2.func3 или (func1(func2(func3())), все эти функции должны принимать только один параметр. Например, если func2 принимает два параметра (func2(param1, param2)), мы не сможем включить её в серию.

Но с практической точки зрения многие функции могут принимать несколько параметров. Так как же нам создавать из них цепочки? Ответ: С помощью каррирования.

Каррирование превращает функцию, которая принимает несколько параметров в функцию, которая принимает только один параметр за один раз. Функция не запустится, пока все параметры не будут переданы.

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

Давайте снова взглянем на наше решение:

Пример 2: обработка функций, бросающих исключения и выход сразу после ошибки

Освещенные темы: Монада «Either»

Монада Maybe подходит нам, чтобы обработать ошибки, связанные с null и undefinedНо что делать с функциями, которым требуется выбрасывать исключения? И как определить, какая из функций в цепочке вызвала ошибку, когда в серии несколько функций, бросающих исключения?

Например, если func2 из цепочки func1.func2.func3... выбросила исключение, мы должны пропустить вызов func3 и последующие функции и корректно обработать ошибку.

Монада Either

Монада Either превосходно подойдет для подобной ситуации.

Пример использования: В примере ниже мы рассчитываем «tax» и «discont» для «items» и в конечном счете вызываем showTotalPrice.

Заметьте, что функции «tax» и «discount» выбросят исключение, если в качестве цены передано не числовое значение. Функция «discount», помимо этого, вернет ошибку в случае, если цена меньше 10.

Давайте посмотрим, как можно реализовать этот пример в функциональном стиле, используя монаду Either.

Either-монада предоставляет два конструктора: «Either.Left» и «Either.Right«. Думайте о них, как о подклассах Either. И «Left«, и «Right» тоже являются монадами. Идея в том, чтобы хранить ошибки или исключения в Left и полезные значения в Right.

Экземпляры Either.Left или Either.Right создаются в зависимости от значения функции.

Так давайте же посмотрим, как изменить наш императивный пример на функциональный.

Шаг 1: Оберните возвращаемые значения в Left и Right. «Оборачивание» означает создание экземпляра класса с помощью оператора new.

Шаг 2: Оберните исходное значение в Right, так как оно валидно.

Шаг 3: Создайте две функции: одну для обработки ошибок, а другую для отображения результата. Оберните их в Either.either (из библиотеки ramda-fantasy.js).

Either.either принимает 3 параметра: обработчик успешного завершения, обработчик ошибок и монаду Either. Сейчас мы можем передать только обработчики, а третий параметр, Either, передать позже.

Как только Either.either получит все три параметра, она передаст третий параметр в обработчик успешного завершения или обработчик ошибок, в зависимости от типа монады: Left или Right.

Шаг 4: Используйте метод chain, чтобы создать цепочку из нескольких функций, выбрасывающих исключения. Передайте результат их выполнения в Either.either (eitherLogOrShow).

Все вместе выглядит следующим образом:

В следующей части мы рассмотрим аппликативные функторы, curryN и Validation Applicative.

Перевод статьи «Functional Programming In JS — With Practical Examples (Part 1)»