Введение в функциональное программирование: парадигмы 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) она создает побочный эффект во внешнем мире, изменяя переменную в этом Мир.

getId — нечистая иллюстрация

Это может быть проблематично, если нам придется отлаживать этот код.

Каково текущее значение id_count ? Какие другие функции изменяют id_count ? Существуют ли другие функции, id_count ?

По этим причинам мы используем только чистые функции в функциональном программировании.

Еще одним преимуществом чистых функций является то, что их можно распараллеливать и запоминать. Взгляните на предыдущие две функции. Их невозможно распараллелить или запомнить. Это помогает в создании производительного кода.

Принципы функционального программирования

Итак, мы узнали, что функциональное программирование зависит от нескольких правил. Они следующие.

  1. Не изменяйте данные
  2. Используйте чистые функции: фиксированный вывод для фиксированных входных данных и никаких побочных эффектов.
  3. Используйте выражения и объявления

Когда мы удовлетворяем этим условиям, мы можем сказать, что наш код функционален.

Функциональное программирование на 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 позволяет гораздо лучше использовать функциональное программирование, чем когда-либо прежде.