Введение в функциональное программирование: парадигмы JavaScript
Опубликовано: 2022-03-11Функциональное программирование — это парадигма построения компьютерных программ с использованием выражений и функций без изменения состояния и данных.
Соблюдая эти ограничения, функциональное программирование стремится писать код, более понятный и более устойчивый к ошибкам. Это достигается за счет отказа от использования операторов управления потоком ( for
, while
, break
, continue
, goto
), которые усложняют выполнение кода. Кроме того, функциональное программирование требует от нас написания чистых, детерминированных функций, которые с меньшей вероятностью будут содержать ошибки.
В этой статье мы поговорим о функциональном программировании с использованием JavaScript. Мы также рассмотрим различные методы и функции JavaScript, которые делают это возможным. В конце мы рассмотрим различные концепции, связанные с функциональным программированием, и поймем, почему они такие мощные.
Однако прежде чем приступить к функциональному программированию, необходимо понять разницу между чистыми и нечистыми функциями.
Чистые и нечистые функции
Чистые функции принимают некоторый ввод и дают фиксированный вывод. Кроме того, они не вызывают побочных эффектов во внешнем мире.
const add = (a, b) => a + b;
Здесь add
— это чистая функция. Это связано с тем, что при фиксированных значениях a
и b
вывод всегда будет одинаковым.
const SECRET = 42; const getId = (a) => SECRET * a;
getId
не является чистой функцией. Причина в том, что он использует глобальную переменную SECRET
для вычисления вывода. Если SECRET
изменится, функция getId
вернет другое значение для того же ввода. Таким образом, это не чистая функция.
let id_count = 0; const getId = () => ++id_count;
Это также нечистая функция, и это тоже по нескольким причинам: (1) она использует нелокальную переменную для вычисления своего вывода и (2) она создает побочный эффект во внешнем мире, изменяя переменную в этом Мир.
Это может быть проблематично, если нам придется отлаживать этот код.
Каково текущее значение id_count
? Какие другие функции изменяют id_count
? Существуют ли другие функции, id_count
?
По этим причинам мы используем только чистые функции в функциональном программировании.
Еще одним преимуществом чистых функций является то, что их можно распараллеливать и запоминать. Взгляните на предыдущие две функции. Их невозможно распараллелить или запомнить. Это помогает в создании производительного кода.
Принципы функционального программирования
Итак, мы узнали, что функциональное программирование зависит от нескольких правил. Они следующие.
- Не изменяйте данные
- Используйте чистые функции: фиксированный вывод для фиксированных входных данных и никаких побочных эффектов.
- Используйте выражения и объявления
Когда мы удовлетворяем этим условиям, мы можем сказать, что наш код функционален.
Функциональное программирование на JavaScript
В JavaScript уже есть некоторые функции, позволяющие выполнять функциональное программирование. Пример: String.prototype.slice, Array.prototype.filter, Array.prototype.join.
С другой стороны, Array.prototype.forEach, Array.prototype.push — нечистые функции.
Можно возразить, что Array.prototype.forEach
не является нечистой функцией по замыслу, но подумайте об этом — с ней невозможно что-либо сделать, кроме изменения нелокальных данных или выполнения побочных эффектов. Таким образом, можно отнести его к категории нечистых функций.
Кроме того, в JavaScript есть объявление const, которое идеально подходит для функционального программирования, поскольку мы не будем изменять какие-либо данные.
Чистые функции в JavaScript
Давайте посмотрим на некоторые из чистых функций (методов), предоставляемых JavaScript.
Фильтр
Как следует из названия, это фильтрует массив.
array.filter(condition);
Условие здесь — это функция, которая получает каждый элемент массива, и она должна решить, сохранять элемент или нет, и возвращать для него истинное логическое значение.
const filterEven = x => x%2 === 0; [1, 2, 3].filter(filterEven); // [2]
Обратите внимание, что filterEven
— это чистая функция. Если бы это было нечисто, то это сделало бы нечистым весь вызов фильтра.
карта
map
сопоставляет каждый элемент массива с функцией и создает новый массив на основе возвращаемых значений вызовов функций.
array.map(mapper)
mapper
— это функция, которая принимает элемент массива в качестве входных данных и возвращает результат.
const double = x => 2 * x; [1, 2, 3].map(double); // [2, 4, 6]
Уменьшать
reduce
уменьшает массив до одного значения.
array.reduce(reducer);
reducer
— это функция, которая принимает накопленное значение и следующий элемент массива и возвращает новое значение. Он вызывается так для всех значений в массиве, по порядку.
const sum = (accumulatedSum, arrayItem) => accumulatedSum + arrayItem [1, 2, 3].reduce(sum); // 6
Конкат
concat
добавляет новые элементы в существующий массив, чтобы создать новый массив. Он отличается от push()
тем, что push()
изменяет данные, что делает их нечистыми.

[1, 2].concat([3, 4]) // [1, 2, 3, 4]
Вы также можете сделать то же самое, используя оператор спреда.
[1, 2, ...[3, 4]]
Объект.назначить
Object.assign
копирует значения из предоставленного объекта в новый объект. Поскольку функциональное программирование основано на неизменяемых данных, мы используем его для создания новых объектов на основе существующих объектов.
const obj = {a : 2}; const newObj = Object.assign({}, obj); newObj.a = 3; obj.a; // 2
С появлением ES6 это также можно сделать с помощью оператора спреда.
const newObj = {...obj};
Создание собственной чистой функции
Мы также можем создать нашу чистую функцию. Давайте сделаем один для дублирования строки n
раз.
const duplicate = (str, n) => n < 1 ? '' : str + duplicate(str, n-1);
Эта функция дублирует строку n
раз и возвращает новую строку.
duplicate('hooray!', 3) // hooray!hooray!hooray!
Функции высшего порядка
Функции высшего порядка — это функции, которые принимают функцию в качестве аргумента и возвращают функцию. Часто они используются для расширения функциональности функции.
const withLog = (fn) => { return (...args) => { console.log(`calling ${fn.name}`); return fn(...args); }; };
В приведенном выше примере мы создаем функцию высшего порядка withLog
, которая принимает функцию и возвращает функцию, которая записывает сообщение в журнал перед запуском обернутой функции.
const add = (a, b) => a + b; const addWithLogging = withLog(add); addWithLogging(3, 4); // calling add // 7
withLog
HOF можно использовать и с другими функциями, и он работает без каких-либо конфликтов или написания лишнего кода. В этом прелесть HOF.
const addWithLogging = withLog(add); const hype = s => s + '!!!'; const hypeWithLogging = withLog(hype); hypeWithLogging('Sale'); // calling hype // Sale!!!
Его также можно вызвать без определения объединяющей функции.
withLog(hype)('Sale'); // calling hype // Sale!!!
карри
Каррирование означает разбиение функции, которая принимает несколько аргументов, на один или несколько уровней функций более высокого порядка.
Возьмем функцию add
.
const add = (a, b) => a + b;
Когда нам нужно его каррировать, мы переписываем его, распределяя аргументы по нескольким уровням следующим образом.
const add = a => { return b => { return a + b; }; }; add(3)(4); // 7
Преимущество каррирования заключается в запоминании. Теперь мы можем запоминать определенные аргументы в вызове функции, чтобы их можно было повторно использовать позже без дублирования и повторного вычисления.
// assume getOffsetNumer() call is expensive const addOffset = add(getOffsetNumber()); addOffset(4); // 4 + getOffsetNumber() addOffset(6);
Это, безусловно, лучше, чем использовать везде оба аргумента.
// (X) DON"T DO THIS add(4, getOffsetNumber()); add(6, getOffsetNumber()); add(10, getOffsetNumber());
Мы также можем переформатировать нашу каррированную функцию, чтобы она выглядела лаконично. Это связано с тем, что каждый уровень вызова каррирующей функции представляет собой однострочный оператор возврата. Поэтому мы можем использовать стрелочные функции в ES6 для рефакторинга следующим образом.
const add = a => b => a + b;
Сочинение
В математике композиция определяется как передача вывода одной функции на вход другой с целью создания комбинированного вывода. То же самое возможно в функциональном программировании, поскольку мы используем чистые функции.
Чтобы показать пример, давайте создадим несколько функций.
Первая функция — это range, которая принимает начальное число a
и конечное число b
и создает массив, состоящий из чисел от a
до b
.
const range = (a, b) => a > b ? [] : [a, ...range(a+1, b)];
Затем у нас есть функция, умножающая, которая берет массив и перемножает все числа в нем.
const multiply = arr => arr.reduce((p, a) => p * a);
Мы будем использовать эти функции вместе, чтобы вычислить факториал.
const factorial = n => multiply(range(1, n)); factorial(5); // 120 factorial(6); // 720
Приведенная выше функция для вычисления факториала похожа на f(x) = g(h(x))
, что демонстрирует свойство композиции.
Заключительные слова
Мы рассмотрели чистые и нечистые функции, функциональное программирование, новые функции JavaScript, которые помогают в этом, и несколько ключевых концепций функционального программирования.
Мы надеемся, что эта часть пробудит ваш интерес к функциональному программированию и, возможно, побудит вас попробовать его в своем коде. Мы уверены, что это станет важным опытом и важной вехой в вашем путешествии по разработке программного обеспечения.
Функциональное программирование — это хорошо изученная и надежная парадигма написания компьютерных программ. С введением ES6 JavaScript позволяет гораздо лучше использовать функциональное программирование, чем когда-либо прежде.