Функциональное программирование. Что это и зачем?
Введение
Сразу уточню: функциональное программирование — это не когда добренько так в один файл функций навалили и радуемся. И вообще: сам факт использования функций не делает программирование функциональным.
Тогда что ж это такое? Это такой вид программирования, в котором функции понимают в математическом смысле, как отображение одного множества в другое. Или проще: функция — это правило, по которому каждому элементу множества 1 соответствует один и только один элемент множества 2.
Наверняка вы уже слышали понятие “чистая функция”? Вот это оно. Исключительно вычисления, без побочных эффектов, если передашь значение 1 — всегда получишь одно и то же значение 2. Очевидно, что их легко тестировать и кэшировать. Но об этом — позже.
Взгляд с другой стороны
Функциональное программирование противоположно императивному — тому, к которому мы привыкли, тому, которое представляет собой последовательность инструкций, выполняемых одна за другой. Функциональное программирование часто считают подвидом программирования декларативного, того, где мы описываем, что мы хотим получить, но не описывая, как. Хороший пример декларативного программирования — язык SQL:
SELECT book.title as title, book.year as year
FROM author
JOIN book ON book.author_id = author.id
WHERE author.name = "Eric A. Blair" AND year >= 1945 AND year <= 1949
| title | year |
|-------------|------|
| 1984 | 1949 |
| Animal Farm | 1945 |
| | |
Видите? Мы не описываем, как достать книги из базы, мы просто описываем, что хотим получить: заголовки и года выпуска книг автора Eric A. Blair, выпущенных с 1945 по 1949 год включительно. В функциональном программировании мы часто делаем так же.
Принципы функционального программирования
Последовательность не имеет значения
Нет разницы, в каком порядке мы напишем подпрограммы; они применятся тогда, когда будет нужно, а не в порядке их написания.
Нет переменных
Точнее, есть, но не в том виде, в котором мы привыкли их видеть. Мы можем объявлять константы, а промежуточные значения хранятся в функциях.
Чистые функции
А почему мы храним промежуточные значения в функциях? А потому что они чистые — всегда вернут один и тот же результат B для параметра А.
Концепции
Функции высших порядков
Функции высших порядков — это такие функции, которые могут принимать другие функции в качестве аргументов и возвращать функции как результат выполнения. Такое есть и активно используется в JS:
const arr = [1, 2, 3];
const plusOne = (num) => num += 1;
const arr2 = arr.map(plusOne) // [2, 3, 4]
Видели? Мы передали в функцию map функцию plusOne (кстати, саму функцию map()
можно рассматривать как монаду — а монады тоже очень характерны для ФП).
Еше один пример:
function sum(a, b, c) {
return a + b + c;
}
function curry (func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
}
};
const curriedSummer = curry(sum);
console.log(typeof curriedSummer) //function
console.log(typeof curriedSummer(1)) // function)
console.log(typeof curriedSummer(1)(2)) // function)
Как мы видим из этого примера, функции в JS могут возвращать функции — из чего, кстати, мы можем сделать вывод, что JS поддерживает функции высших порядков. Кстати — это еще и пример каррирования функций, еще одного характерного для функционального программирования приемов, поддерживаенмых JS. О каррировании можно подробнее почитать здесь.
Рекурсия
В функциональных языках программирования циклы обычно представленны в виде рекурсии. А в строгих языках программирования циклов и вовсе нет. Рекурсивные функции вызывают сами себя, пока не достигнут результата. Для этого может потребоваться большой стек, но такая оптимизация, как хвостовая рекурсия, помогает этого избежать
Хвостовая рекурсия — это такая рекурсия, при которой рекурсивный вызов является последней операцией перед возвратом из функции. Такую рекурсию компилятор может заменить на итерацию, и это поможет избежать переполнения стека.
Начиная с ES6, JavaScript поддерживает хвостовую рекурсию (только в strict mode). Вот такой пример будет преобразован компилятором V8 в итерацию:
"use strict";
function foo(x, acc) {
if (x < 2) {
return acc;
}
return foo(x - 1, acc * x);
}
foo(100000, 1);
Особенности функционального программирования
Основная особенность функционального программирования — то, что в процессе выполнения она не имеет состояния. В императивных языках есть значения переменных, результаты побочных эффектов — всего этого в ФП нет. Следствием этого является то, что функциональная программа в чистом виде не может изменять уже имеющиеся у нее данные, будучи вынуждена порождать новые путем копирования или расширения старых. Следствием того же является использование рекурсии вместо циклов.
Сильные стороны
Надежность
Программа без состояния максимально надежна, да и данные меняться не могут.
Удобство юнит-тестирования
Писать юнит-тесты для чистых функций — одно удовольствие, ведь нам не придется имитировать побочные эффекты (мокать работу с БД, файловой системой и так далее).
Параллельные вычисления
Если все функции не содержат побочных эффектов, почему бы не выполнять их параллельно?
Недостатки
По сути, недостатки функционального программирования следуют из его же особенностей. К примеру, если мы не можем изменять данные, то вынуждены постоянно аллоцировать новую память, и нам крайне важен эффективный сборщик мусора (как сборка мусора устроена в JS, можно почитать здесь). А нестрогая модель вычислений создает проблемы с операциями ввода/вывода, где порядок исполнения кода важен (это решается с помощью монад).
Выводы
Функциональная парадигма как минимум неплохо расширяет кругозор, и во многих случаях гораздо эффективнее имреративной. Раз уж мы тут JS изучаем, то нужно сказать, что JS поддерживает очень многое из ФП: монады, функции/классы высшего порядка и каррирование, хвостовую рекурсию. Чистые функции тоже никто писать не запрещает. Поэтому на JS вполне можно писать в функциональном стиле :)
Stay tuned! Статей будет еще очень много :)
Интересный пост?
Вот еще похожие:
- Событийно-ориентированная архитектура и Node.js Events
- Реактивное программирование: теория и практика
- Как и зачем писать тесты?
- Профилирование Node.js-приложений
- Docker: что, зачем и почему