Статья будет полезна тем, кто хочет приоткрыть для себя дверь в мир функционального программирования. В начале рассмотрим отличия ООП и ФП, затем как сочетать функции друг с другом и в конце применим знаменитые фишки функционального подхода.
Давайте для начала разберемся с основными особенностями парадигмы функционального программирования, для простоты восприятия оттолкнемся от объектно ориентированного подхода.
Отличия объектно ориентированного и функционального подходов
Что такое ООП?
- Работаем с объектами, то есть классами и экземплярами этих классов;
- Пишем в императивном стиле;
- Классы тесно связаны между собой;
- Данные и методы по работе с классом лежат в самом классе. Эти данные должны быть инкапсулированы и доступ к ним должен осуществляться через геттеры/сеттеры и методы этого класса;
- Данные являются изменяемыми. Мы можем их изменить в любой момент работы нашего кода.
Что такое ФП?
- Работаем с функциями;
- Пишем в декларативном стиле;
- Каждая функция - самостоятельная единица;
- Используем чистые функции. Они не производят побочных эффектов, то есть не изменяют входные параметры,ничего не выводят, ничего не присваивают внешним переменным, не вызывают нечистые функции, не обращаются к внешним 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.
На простых примерах мы рассмотрели, как функциональное программирование помогает писать более чистый код, который можно переиспользовать.