Статья будет полезна тем, кто хочет приоткрыть для себя дверь в мир функционального программирования. В начале рассмотрим отличия ООП и ФП, затем как сочетать функции друг с другом и в конце применим знаменитые фишки функционального подхода.
Давайте для начала разберемся с основными особенностями парадигмы функционального программирования, для простоты восприятия оттолкнемся от объектно-ориентированного подхода.
Отличия объектно-ориентированного и функционального подходов
Что такое ООП?
- Работаем с объектами, то есть классами и экземплярами этих классов;
- Пишем в императивном стиле;
- Классы тесно связаны между собой;
- Данные и методы по работе с классом лежат в самом классе. Эти данные должны быть инкапсулированы и доступ к ним должен осуществляться через геттеры/сеттеры и методы этого класса;
- Данные являются изменяемыми. Мы можем их изменить в любой момент работы нашего кода.
Что такое ФП?
- Работаем с функциями;
- Пишем в декларативном стиле;
- Каждая функция - самостоятельная единица;
- Используем чистые функции. Они не производят побочных эффектов, то есть не изменяют входные параметры, ничего не выводят, ничего не присваивают внешним переменным, не вызывают нечистые функции, не обращаются к внешним API, работают детерминировано;
- Данные являются иммутабельными.
Основное отличие в том, что в ФП важно не то, как мы хотим что-то получить, а что мы хотим получить.
С чего начать, чтобы мыслить в парадигме функционального программирования?
Сперва попробуем вместо циклов использовать итерационные функции. Применим те, которые дает нам Ramda :
Для операций с массивами, в Ramda имеются и другие:
Начинаем соединять функции
В предыдущих примерах мы применяли find для нахождения первого нечетного числа в массиве. Если бы у нас была задача найти первое четное число, мы создали бы isEven и использовали ее. Но мы знаем, что любое четное число не является нечетным. Здесь мы переиспользуем isOdd. В Ramda есть метод complement, который является функцией высшего порядка. Он принимает в себя isOdd. На выходе получится новая функция, возвращающая true, когда наша оригинальная isOdd возвращает false, и наоборот:
Она делает то же самое, что оператор !. Давайте запишем эту функции в переменную с соответствующим именем, чтобы мы могли переиспользовать её:
Существуют аналоги операторов && и || — функции both / either соответственно:
В Ramda также присутствуют allPass и anyPass :
Допустим, нужно обработать данные друг за другом, конвейером. Например, перемножить два числа, к произведению прибавить единицу, а результат возвести в квадрат:
Заметьте, что каждая последующая функция применяется к результату предыдущей в конвейере. Ramda предоставляет инструмент pipe, где принимает список из нескольких функций, затем возвращает новую, которая ожидает на вход такое же количество параметров, что и первая в списке. После передает результат работы первой во вторую, и так далее. Результат работы заключительной функции в списке будет являться результатом прохождения всего конвейера. Есть ограничение, каждая функция после первой должна ожидать только один аргумент.
Все функции в Ramda автоматически каррированы. Это означает, что вы можете вызывать функцию с меньшим количеством параметров, чем она требует, и она вернет «частично примененную». Функции будут продолжать возвращать новые пока все параметры не будут переданы. И когда будет передан последний параметр произойдет вызов последней функции, который вернет финальный результат.
В Ramda так же есть функция curry, которая позволяет каррировать любую другую.
Каррированые функции позволяют нам делать две важные вещи в функциональном программировании — специализацию и композицию.
Специализация происходит от возможности каррированых функций быть «частично примененными». Мы можем сохранить эти частично примененные методы в переменные и использовать позже.
Другая важная концепция — это композиция функций. Мы можем комбинировать так, чтобы получить новую функцию. Тут мы создали новую, которая возвращает результат работы обеих double и increment.
Мы сделали композицию. Выглядит неплохо, но что будет если мы захотим сделать композицию из более чем двух?
Теперь это выглядит плохо. Но мы уже знаем pipe и с ее помощью можем записать в более человеко-читаемой манере.
Это же можно применить и к каррированым. Возьмем для примера встроенные функции Ramda, хотя это будет справедливо для любой из каррированых.
В данном примере multiply и add принимают 2 параметра, но мы передали только один. Каждая из них вернула новую, которая ждет последнего параметра. Таким образом, когда мы вызываем mathPipe со значением 10, первая, частично примененная, возвращает нам 40 и затем это значение передается во вторую, частично примененную и в итоге мы получим 42.
На простых примерах мы рассмотрели, как функциональное программирование помогает писать более чистый код, который можно переиспользовать.