Функциональное программирование с примерами на JavaScript. Часть первая. Основные техники функционального программирования
Функциональное программирование простым языком: о каррировании, чистых функциях, fantasy-land, функторах и монадах. С примерами реализации на JavaScript.
47К открытий48К показов
Рассказывает rajaraodv
Функциональное программирование, или ФП, может изменить стиль вашего написания программ к лучшему. Но освоить его довольно непросто, а многие посты и туториалы не рассматривают детали (вроде монад, аппликативных функторов и т.п.) и не предоставляют примеры из практики, которые помогли бы новичкам использовать мощные техники ФП ежедневно. Поэтому я решил написать статью, в которой освещу основные идеи ФП.
В первой части вы изучите основы ФП, такие как каррирование, чистые функции, fantasy-land, функторы, монады, Maybe-монады и Either-монады на нескольких примерах.
Функциональное программирование — это стиль написания программ через составление набора функций.
Основной принцип ФП — оборачивать практически все в функции, писать множество маленьких многоразовых функций, а затем просто вызывать их одну за другой, чтобы получить результат вроде (func1.func2.func3)
или в композиционном стиле func1(func2(func3()))
.
Кроме этого, структура функций должна следовать некоторым правилам, описанным ниже.
Итак, у вас могло возникнуть несколько вопросов. Если любую задачу можно решить, объединяя вызовы нескольких функций, то:
- Как реализовать условия (if-else)? (Совет: используйте монаду Either);
- Как перехватить исключения типа Null Exception? (В этом может помочь монада Maybe);
- Как убедиться в том, что функция действительно “многоразовая” и может использоваться в любом месте? (Чистые функции);
- Как убедиться, что данные, которые мы передаем, не изменяются, чтобы мы могли бы использовать их где-то еще? (Чистые функции, иммутабельность);
- Если функция принимает несколько значений, но цепочка может передавать только одно значение, как мы можем сделать эту функцию частью цепочки? (Каррирование и функции высшего порядка).
Чтобы решить все эти проблемы, функциональные языки, вроде 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-fantasy, immutable-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.
- Если есть объект, который может равняться null или иметь нулевые свойства, создаем экземпляр монады из него.
- Используем библиотеки, вроде ramdajs, чтобы получить значение из монады и работать с ним.
- Возвращаем значение по умолчанию, если данные равняются 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.
47К открытий48К показов