Когда дело доходит до парадигм программирования, наиболее популярным выбором является объектно-ориентированное программирование. Нетрудно найти разработчиков, которые могут описать, что делает класс или как создать экземпляр объекта.

Функциональное программирование существует гораздо дольше, чем ООП. Лучшим примером является LISP, первая спецификация которого была написана в 1958 году. Однако, в отличие от ООП, не так просто найти разработчиков, которые могут понять функциональные концепции, такие как Purity, Currying или Function Composition.

Javascript не является языком функционального программирования или, по крайней мере, не является его основной ориентированной парадигмой. Это не означает, что мы не можем работать функционально, используя такие библиотеки, как Lodash, Underscore, RambdaJS или используя только Vanilla Javascript.

В этом году мне довелось прочитать книгу Федерико Кереки Освоение функционального программирования на Javascript. Я лично рекомендую эту книгу всем, кто хочет глубоко понять концепции функционального программирования и их приложения, использующие Javascript. В этой статье я объясню, чему научился из книги Кереки. Я не собираюсь повторять все из книги - Если хотите, прочтите книгу! - а скорее предложите резюме и основные моменты.

ПРИМЕЧАНИЕ. Эта статья адресована разработчикам Javascript среднего / продвинутого уровня.

1. Функции как первоклассные объекты

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

В Javascript вы можете определять функции несколькими способами. Я считаю удобным всегда использовать форму стрелки - если вы не хотите использовать состояние this. Пример:

const add = (a, b) => a + b;

Если вы опытный JS-разработчик, скорее всего, вы сталкивались с функциями обратного вызова, подобными тем, которые используются с setTimeout и setInterval. Это прекрасные примеры использования функций в качестве параметров. Другой пример:

var doSomething = function(status) {
// Doing something
};
var foo = function(data, func) { // Passing function as a parameter
    func(data);
}
foo("some data", doSomething);

2. Важность чистых функций

Работа с функциями - это не единственное, что вам нужно делать, чтобы работать функционально. Вам также необходимо, чтобы ваши функции были чистыми. Но что это значит? Что ж, по словам Федерико Кереки, у вас есть чистая функция, когда она соответствует следующим критериям:

- При одинаковых аргументах функция всегда вычисляет и возвращает один и тот же результат.

- При вычислении своего результата функция не вызывает никаких наблюдаемых побочных эффектов, включая вывод на устройства ввода-вывода, изменение объектов, изменение состояния программы за пределами функции и т. Д.

Давайте посмотрим на пример:

const getRectangleArea = (sideA, sideB) => sideA * sideB;
console.log(getRectangleArea(2, 3)); // 6

Эта функция является чистой, потому что она всегда будет возвращать результат «6» для заданных аргументов «2» и «3». Эта функция вообще не влияет на свой внешний контекст. Внутри него все происходит, и он дает новый результат без изменения (мутации) своих аргументов. Таким образом, мы можем с уверенностью сказать: У него нет побочных эффектов, поэтому функция чистая.

Вот и все. Ваша функция чиста, когда у нее нет побочных эффектов. Если у него нет побочных эффектов, значит, ваша функция чиста. Легко, правда? Но что, черт возьми, за побочный эффект? Вот список возможных побочных эффектов, с которыми вы можете столкнуться:

  • Использование глобальных переменных (если они не являются константами).
  • Мутирующие объекты, полученные в качестве аргументов.
  • Такие операции, как выполнение любого типа ввода-вывода, работа с файловой системой, изменение файловой системы, обновление базы данных, вызов внешнего API и т. Д.

И последнее, но не менее важное: использование функции, которая оказывается нечистой. Федерико говорит, что нечистые функции «заразительны». Поэтому, если ваша функция использует что-то, что вызывает побочный эффект, ваша функция станет нечистой.

3. Работа с побочными эффектами

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

Итак, как мы можем использовать нечистые функции в то же время, когда мы работаем функционально? Во-первых, вам нужно принять тот факт, что невозможно достичь 100% чисто функционального программирования в вашей повседневной работе. Тем не менее, это не должно быть вашей целью. Как говорит Федерико в своей книге:

Однако не попадайтесь в ловушку, рассматривая FP как цель! Думайте о FP только как о средстве достижения цели, как и обо всех программных инструментах. Функциональный код хорош не только потому, что он функциональный… а писать плохой код с помощью FP так же возможно, как и с любыми другими методами!

У Федерико для этого есть название: «Функциональное программирование Сорта». В общем, наша цель должна заключаться в том, чтобы отделить нечистые части нашего кода от чистых частей. Вот пример:

Этот Node-скрипт читает текстовый файл, считает его слова и отправляет результат во внешний API - ну, не совсем «настоящий» API, но давайте представим, что он настоящий - есть только одна чистая функция (подсчет слов), остальные нечисты. Таким образом, мы можем отделить нечистые части нашего кода от чистых.

Прошлая стратегия работает хорошо. Однако вызовы функций выполняются в императивной форме (инструкции в последовательности). Более функциональная альтернатива - вставить нечистые функции в чистые. Давайте посмотрим на другой пример, объясняющий это.

Предположим, мы хотим сгенерировать случайные целые числа в диапазоне. Используя решение, предложенное в документации Mozilla, вы можете сделать что-то вроде этого:

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min)) + min;
}

Проблема с этим решением - примесь методаMath.random. Итак, чтобы отделить этот метод от функции getRandomInt, мы можем сделать что-то вроде этого:

function getRandomInt(min, max, random = Math.random) {
  return Math.floor(random() * (max - min)) + min;
}

Вот и все. Мы разъединили нашу getRandomInt функцию. Обратите внимание, как мы используем параметры ES6 по умолчанию, поэтому нам не нужно передавать Math.random ссылку при каждом вызове. Однако я знаю, о чем вы думаете ...

Вы сказали мне, что если чистая функция использует что-то нечистое, эта тоже станет нечистой.

Да, ты прав. Эта функция не будет вести себя чисто, пока переданная функция не чистая. Однако это дает нам большое преимущество при запуске тестов. Давайте посмотрим на этот пример с использованием Jest:

В приведенном выше примере мы переопределяем функцию по умолчанию, чтобы использовать ту, которую мы передаем в качестве третьего параметра (стрелочная функция, которая возвращает только значение). Делая это, мы делаем случайную функцию детерминированной и, следовательно, чистой, что значительно упрощает нам выполнение тестовых утверждений.

4. Использование функций высокого порядка

Если вы какое-то время работали с JS, скорее всего, вы наткнулись на такие функции, как map, filter, reduce и т. Д. Это хорошие примеры функций высокого порядка или HOF. Это функции, которые принимают другие функции в качестве параметров. Они могут либо вернуть новую функцию, либо результат в зависимости от переданной ей функции.

В следующей части я опишу карту, фильтр и другие встроенные функции HOF. Но пока мы собираемся сосредоточиться на создании собственной реализации HOF.

Время измерения

Допустим, вы хотите записать время, необходимое для выполнения функции. Ваша первая идея может заключаться в следующем:

const calculateRectangleArea = (sideA, sideB) => sideA * sideB;
(() => {
  const startTime = Date.now();
  const rectangleArea = calculateRectangleArea(2, 3);
  const time = Date.now() - startTime;
  console.log(`Function calculateRectangleArea took ${time} to complete`);
})();

Это решение работает хорошо, но что, если мы хотим измерить время других функций? Придется ли нам повторять этот код снова и снова? Ни в коем случае! Итак, давайте реализуем HOF, который принимает любую функцию в качестве параметра и генерирует новую функцию, которая регистрирует время выполнения. Мы будем называть это addTiming.

Как вы можете видеть выше, мы реализуем HOF, который принимает три параметра, но только один из них является обязательным (первый). Какую функцию мы хотим обернуть, вторая - это функция, используемая для получения текущего времени, а третья - функция, используемая для регистрации времени.

Эта функция сначала получит текущее время, затем вызовет целевую функцию с соответствующими аргументами - которые мы получаем с помощью оператора распространения - и сразу после этого она выполнит и запишет время, необходимое для выполнения целевой функции. Наконец, он вернет соответствующий результат. Также есть некоторая обработка ошибок на тот случай, если функция не завершится должным образом.

Функции запоминания

Запоминание (или кеширование результатов) - еще одна интересная особенность HOF, которую обычно упускают из виду. Как мы уже упоминали, при работе с Чистыми функциями мы можем быть уверены, что для любого заданного набора параметров он всегда будет возвращать один и тот же конкретный результат. Это означает, что мы можем использовать для сохранения этих результатов в памяти, чтобы потом использовать их в будущих вызовах. Этот метод мемоизации особенно важен при работе с дорогостоящими вычислениями, на выполнение которых уходит слишком много времени.

Возьмем для примера следующую функцию Фибоначчи:

const fib = n => {
  if (n === 0) {
    return 0;
  } else if (n === 1) {
    return 1;
  } else {
    return fib(n - 2) + fib(n - 1);
  }
};

Эта функция Фибоначчи использует рекурсию и увеличивает время выполнения по мере увеличения входных данных. Мы можем использовать нашу недавно созданную функцию createdaddTiming, чтобы убедиться, что:

addTiming(fib)(10); // fib: Normal execution - 0
addTiming(fib)(20); // fib: Normal execution - 1
addTiming(fib)(30); // fib: Normal execution - 11
addTiming(fib)(40); // fib: Normal execution - 1447
addTiming(fib)(50); // fib: Normal execution - 181611

Время, показанное выше, может отличаться от того, которое вы видите на своем ПК, но если у вас нет квантового компьютера - а я уверен, что нет - выполнение этой функции Фибоначчи с входом 50 займет хорошее время.

Итак, как мы можем это оптимизировать? Конечно, используя мемоизацию. Но вместо того, чтобы реализовывать собственное решение для запоминания - и изобретать колесо - мы собираемся использовать HOF, который предоставляет Lodash.

ПРИМЕЧАНИЕ: Если вы никогда не использовали Lodash, вы можете проверить его документацию здесь.

Полное решение с живым примером находится ниже:

Как видите, мы используем let вместо const для определения функции Фибоначчи. Мы делаем это, потому что нам нужно, чтобы исходная ссылка на функцию была перезаписана мемоизированным вызовом. В противном случае это не сработает.

Давайте сравним оба результата выполнения. Я запустил эти примеры с помощью CodeSandbox.io:

Как видите, мемоизированная функция почти не занимает времени (менее 1 мс) для каждого использованного ввода. Тогда как неизмененная функция занимает до 182 248 миллисекунд (более 2 минут). Это действительно разительная разница.

5. Избегайте петель, работая декларативно.

Как я упоминал в предыдущей части, Javascript уже учитывает ряд встроенных функций высокого порядка (HOF). Большинство этих функций используются для обработки массивов или коллекций объектов.

Если вы читаете эту статью, скорее всего, вы уже опытный разработчик JS. Так что я не буду тратить много времени на подробное объяснение каждого из них - это было бы чертовски скучно.

Я опишу следующие функции:

  • Уменьшать
  • карта
  • Для каждого
  • Фильтр
  • Находить
  • Все и некоторые

ПРИМЕЧАНИЕ. Переходите к части 6, если вы уже знаете, как работают все эти методы.

Расчет результатов с помощью метода Reduce

Предположим, у вас есть массив чисел, и вы хотите вычислить его среднее значение. Наилучший функциональный способ добиться этого - использовать встроенный HOF reduce. Например:

const getAverage = myArray =>
  myArray.reduce((sum, val, ind, arr) => {
    sum += val;
    return ind === arr.length - 1 ? sum / arr.length : sum;
  }, 0);
console.log("Average:", getAverage([22, 9, 60, 12, 4, 56])); 
// Average: 27.166666666666668

Метод reduce() выполняет функцию редуктора (которую вы предоставляете) для каждого элемента массива, в результате чего получается одно выходное значение. (Ссылка Mozilla)

Итак, в приведенном выше примере мы реализуем функцию, которая получает массив чисел. Затем мы применяем метод уменьшения к этому массиву и передаем функцию (reducer), которая суммирует все значения массива. Как только будет достигнут последний из них, мы просто вернем деление (среднее), используя длину массива.

Создание новых массивов с помощью метода Map

Предположим, у вас есть следующий массив объектов, содержащий широту и долготу нескольких стран.

const markers = [
  { name: "UY", lat: -34.9, lon: -56.2 },
  { name: "AR", lat: -34.6, lon: -58.4 },
  { name: "BR", lat: -15.8, lon: -47.9 },
  { name: "BO", lat: -16.5, lon: -68.1 }
];

Теперь предположим, что мы хотим создать массив только со значениями широты. Этого легко добиться, используя метод map следующим образом:

console.log("Lat values:", markers.map(x => x.lat));
// Lat values: [ -34.9, -34.6, -15.8, -16.5 ]

Метод map() создает новый массив, заполненный результатами вызова предоставленной функции для каждого элемента в вызывающем массиве. (Ссылка Mozilla)

В приведенном выше примере предоставленная функция возвращает только значения широты.

Цикл с использованием метода ForEach

Иногда единственное, что мы хотим сделать, - это перебрать серию значений или объектов. Мы можем использовать forEach в таких ситуациях. Например, предположим, что мы хотим записать все данные массива маркеров:

const logMarkersData = markers => {
  console.log("Data provided:");
markers.forEach(marker => console.log(`Country: ${marker.name} Latitude: ${marker.lat} Longitude: ${marker.lon}`));
};
logMarkersData(markers);
/*
Data provided:
Country: UY Latitude: -34.9 Longitude: -56.2
Country: AR Latitude: -34.6 Longitude: -58.4
Country: BR Latitude: -15.8 Longitude: -47.9
Country: BO Latitude: -16.5 Longitude: -68.1
*/

Метод forEach() выполняет предоставленную функцию один раз для каждого элемента массива. (Ссылка Mozilla)

Этот пример довольно легко понять. Метод forEach будет перебирать каждый элемент и регистрировать полученные данные.

Фильтрация элементов массива с помощью метода Filter

Давайте продолжим использовать тот же массив маркеров, который мы использовали ранее. Предположим, мы хотим отфильтровать данные из стран, у которых начальная буква «B». В этом вам поможет метод filter:

console.log(
  "Data of countries starting with B:",
  markers.filter(mark => mark.name.charAt(0) === "B")
);
/*
Data of countries starting with B: [ { name: 'BR', lat: -15.8, lon: -47.9 }, { name: 'BO', lat: -16.5, lon: -68.1 } ]
*/

Метод filter() создает новый массив со всеми элементами, прошедшими проверку, реализованную предоставленной функцией. (Ссылка Mozilla)

Поиск определенного элемента с помощью методов Find и FindIndex

Теперь ситуация еще более конкретная. Нам нужны только данные из Бразилии (BR). Итак, мы используем метод find:

console.log("Brazil Data:", markers.find(m => m.name === "BR"));
// Brazil Data: { name: 'BR', lat: -15.8, lon: -47.9 }

Метод find() возвращает значение первого элемента в предоставленном массиве, который удовлетворяет предоставленной функции тестирования. (Ссылка Mozilla)

Но что, если вы хотите знать только индекс? Вместо этого мы бы использовали метод findIndex.

console.log("Brazil Data Index:", markers.findIndex(m => m.name === "BR"));
// Brazil Data Index: 2

Метод findIndex() возвращает индекс первого элемента в массиве, который удовлетворяет предоставленной функции тестирования. В противном случае возвращается -1, указывая на то, что ни один элемент не прошел проверку. (Ссылка Mozilla)

Объединение логических операций с помощью метода Every и Some

Я столкнулся с этими двумя методами совсем недавно. Они особенно полезны, когда вам нужно определить, соответствуют ли все элементы массива определенной логике. Это эквивалентно последовательному использованию оператора И / ИЛИ. Поэтому вместо того, чтобы делать что-то вроде этого:

if (arr[0] > 0 && arr[1] > 0 ..... arr[n] > 0) {...}

Вы можете сделать что-то вроде этого:

if (arr.every(item => item > 0)) {...}

То же самое относится к оператору OR, но на этот раз с использованием метода some.

Давайте посмотрим на это на примере. Представьте, что у нас есть последовательность чисел (например, 4, 8, 15, 16, 23 и 42), и мы хотели бы определить, есть ли хотя бы одно четное число или нет. В этом случае нам нужно использовать метод some:

const lostNumbers = [4, 8, 15, 16, 23, 42];
console.log(
  "Does it contain even numbers?",
  lostNumbers.some(n => n % 2 === 0)
);
// Does it contain even numbers? true

Метод some() проверяет, проходит ли хотя бы один элемент в массиве тест, реализованный предоставленной функцией. Возвращает логическое значение.

С другой стороны, если мы хотим определить, все ли номера массива четны, нам нужно будет использовать метод theevery следующим образом:

console.log("Are all even numbers?", lostNumbers.every(n => n % 2 === 0));
// Are all even numbers? false

Метод every() проверяет, все ли элементы в массиве проходят проверку, реализованную предоставленной функцией. Возвращает логическое значение.

6. Использование композиции функций для объединения функций

В идеале наши функции должны быть небольшими. Они должны уметь делать только одно и делать это правильно. Итак, как мы можем объединить все эти «маленькие» функции для совместной работы? Один из способов добиться этого - использовать Композицию функций. Это звучит как причудливая математическая концепция, и почему-то так и есть. Однако это не так уж и сложно понять. Он заключается в том, чтобы сделать результат одной функции параметром следующей.

Функциональная композиция бывает двух видов: конвейерная обработка и композиция. Они почти одинаковы, но разные. Один работает слева направо, а другой - в противоположном направлении.

Конвейерная обработка

ПРИМЕЧАНИЕ. В книге Федерико Кереки он учит, как реализовать «конвейерную» функцию с нуля. Но здесь мы собираемся использовать решения, предоставленные Lodash.

С Lodash у нас есть две альтернативы. Первый - с использованием метода flow из стандартного API, а второй - с использованием метода pipe из FP API. Давайте рассмотрим пример обоими способами.

Предположим, нам нужно подключиться к устаревшей веб-службе (SOAP WS или аналогичной), которая по-прежнему использует XML в качестве формата данных. Этот WS предоставляет информацию о некоторых книгах, подобную приведенной ниже:

Наша задача следующая. Возьмите ответ выше и:

  • Сортировка книг по цене (в порядке возрастания) и…
  • Распечатайте информацию в формате JSON (используя console.log)

Оказывается, (в JS) работа с данными в формате XML - это полная головная боль в **. К счастью для нас, есть несколько библиотек, которые мы можем использовать для преобразования XML в удобный объект JS. В этом примере мы собираемся использовать один из них под названием xml-js.

Во-первых, давайте возьмем данные из «веб-службы». К сожалению, у нас его нет под рукой. Итак, давайте притворимся:

// Let's pretend this is a Webservice call
const getingWsData = () =>
   Promise.resolve(fs.readFileSync(`${__dirname}/books.xml`, "utf8"));

Затем давайте извлечем необходимую информацию из XML-ответа. Мы собираемся использовать служебную функцию Lodash под названием get, чтобы легко получать значения, предоставляя путь к объекту, метод map, который мы уже описывали в предыдущей части, и метод библиотеки xml2js для анализа исходного текста XML.

const transformToJsObject = xmlData =>
  _(xmlJs.xml2js(xmlData, { compact: true })) // Transforming to JS
    .get("Library.Book") // Getting Books reference
    .map(book => ({ // Interating books array to generate new one
      author: book.Author._text,
      title: book.Title._text,
      year: Number(book.Year._text),
      price: Number(book.Price._text)
    }));

ПРИМЕЧАНИЕ. Я не включаю никакой проверки данных XML. Итак, предположим, что он составлен правильно, а значения года и цены числовые. Но вы, вероятно, захотите перепроверить это для производственного приложения.

Наконец, давайте отсортируем по цене. Здесь мы могли бы использовать встроенный метод sort (ссылка здесь). Но он изменяет целевой массив, что делает нашу функцию нечистой. Поэтому нам лучше использовать эквивалент Lodash sortBy следующим образом:

const sortByPrice = booksArray => _.sortBy(booksArray, "price");

Мы уже определили все необходимые функции для выполнения поставленной задачи. Но как их соединить вместе? Первое, что вам нужно сделать, это сделать это в хорошо известной повелительной форме:

let booksArray = transformToJsObject(xmlData);
booksArray = sortByPrice(booksArray);
const jsonData = JSON.stringify(booksArray);
console.log(jsonData);

Он работает отлично, но требует слишком много строк! Кроме того, обратите внимание, сколько промежуточных переменных / констант нам нужно использовать. У вас может возникнуть идея упростить все, сделав что-то в одной строке, например:

console.log(JSON.stringify(sortByPrice(transformToJsObject(xmlData))));

Но теперь все превращается в ад нечитаемых скобок. Итак, давайте применим технику конвейерной обработки. Сначала стандартным методом Lodash flow:

_.flow(
   transformToJsObject,
   sortByPrice,
   JSON.stringify,
   console.log
)(xmlData);

А теперь с FP Lodash pipe:

fp.pipe(
   transformToJsObject,
   sortByPrice,
   JSON.stringify,
   console.log
)(xmlData);

Как видите, обе функции высшего порядка практически идентичны. Оба получают свои аргументы в одном и том же порядке, слева направо, и самая левая функция будет той, которая вызывается первой. Вот полный пример:

Составление

Как я упоминал ранее, компоновка работает очень похоже на конвейерную обработку. Итак, давайте рассмотрим тот же пример, но на этот раз с использованием compose и flowRight.

Как видите, методы flowRight и compose работают аналогично методам flow и pipe. С той лишь разницей, что теперь порядок параметров инвертирован (справа налево). Но выходной результат точно такой же, как и раньше.

Увидев это, вы можете задаться вопросом: Какой из них мне использовать? И ответ - тот, который вам больше нравится! По моему личному мнению, я предпочитаю использовать конвейерную обработку, поскольку мы обычно читаем слева направо, поэтому для меня естественнее двигаться в этом направлении. Но другие люди могут предпочесть использовать составление, поскольку начальный параметр ближе к функции, которая его получает. Какой бы вариант вы ни использовали, это нормально, если вы понимаете, что делаете.

7. Возможность повторного использования с каррированием

В прошлой части мы увидели, как мы можем комбинировать функции для совместной работы с помощью композиции функций. Теперь позвольте мне задать вам вопрос по этому поводу:

Вы обратили внимание на особенности этих функций?

Я говорю о функциях, используемых в конвейерах и композиторах. Вы что-нибудь в них заметили? Что ж, оказывается, у них всегда был один-единственный аргумент. Поскольку предыдущая функция могла иметь только одно возвращаемое значение. Итак, чтобы наши функции работали с конвейерами или композиторами, они должны быть унарными (один аргумент) по умолчанию. Каррирование - это простой способ преобразовать неунарные функции в унарную форму.

Давайте посмотрим на это на примере. Здесь вы можете увидеть функцию, которая складывает три числа, ничего нового:

const sum = (a, b, c) => a + b + c;

Каррированная версия этой функции будет выглядеть примерно так:

const sum = (a) => (b) => (c) => a + b + c;

Теперь у нас есть функция, которая возвращает функцию, которая возвращает другую функцию. Чтобы назвать эту карри-версию слова «сумма», вы можете сделать следующее:

sum(1)(2)(3); // Result is 6

Я знаю, о чем ты думаешь ... Какого черта мы от этого выиграем? Ответ прост: Вы можете создавать каррированные версии своих функций, если их начальные параметры не меняются в течение нескольких вызовов. Например, предположим, что у вас есть функция, которая рассчитывает скидки с учетом определенного процента. Это выглядело бы так:

comesconst calculateDiscount = (discount, price) => price - (price * discount) / 100;
console.log(calculateDiscount(10, 2000)); // 1800

А теперь давайте представим ситуацию, когда нам нужно всегда применять одну и ту же скидку к разным товарам (с разными ценами). Это означало бы повторение одного и того же первого параметра несколько раз:

console.log(calculateDiscount(10, 2000)); // 1800
console.log(calculateDiscount(10, 500)); // 450
console.log(calculateDiscount(10, 750)); // 675
console.log(calculateDiscount(10, 900)); // 810

Вероятно, нет ничего страшного в том, чтобы повторять один единственный параметр несколько раз, но представьте себе ситуации, когда количество повторяющихся параметров больше (2, 4, 5 и т. Д.). Итак, давайте применим каррирование к этой calculateDiscount функции. Для этого мы собираемся использовать метод thecurry из Lodash:

Как вы можете видеть выше, мы применили каррирование к функции calculateDiscount, и в то же время мы передали первый параметр (процент скидки) в получившуюся функцию. Теперь у нас есть новая функция, единственная цель которой - рассчитать 10% скидку на любую заданную цену.

Конечно, это не единственный практический случай, когда можно использовать каррирование. Подумайте о ситуациях, когда вам нужно создать несколько слушателей для одного события. Вы можете применить каррирование к функции, отвечающей за добавление слушателей, и использовать эту (каррированную) функцию по мере необходимости. Подобно этому, есть много других примеров.

8. Возможность повторного использования с частичным применением

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

Давайте рассмотрим новый пример, чтобы увидеть, как это работает. Предположим, у вас есть простой экземпляр регистратора Winston, подобный следующему:

const logger = winston.createLogger({
  transports: [new winston.transports.Console()]
});

ПРИМЕЧАНИЕ. Winson - это библиотека для простого управления журналами NodeJS. Если вы хотите узнать о нем больше, посмотрите его репозиторий на github здесь.

Теперь предположим, что мы разрабатываем систему, состоящую из нескольких этапов, например конвейер. И мы хотели бы регистрировать сообщения, соответствующие определенному шагу, возможно, потому, что мы хотим обеспечить некоторую «отслеживаемость» для нашей системы. Например: step: "Order Preparation", message: "Assigning Carrier to order XXX". Давайте определим функцию, которая делает именно это:

const logMessage = (level, message, step, loggerFn = logger) =>
  loggerFn[level]({ message, step });

Как видите, мы внедряем экземпляр регистратора в функцию logMessage. Теперь мы можем начать регистрацию сообщения, вызвав эту функцию следующим образом:

logMessage("info", "Assigning Carrier to order XXX", "Order Preparation")

Проблема с этим подходом заключается в том, что если нам нужно регистрировать несколько сообщений на одном шаге и с одним и тем же (информационным) уровнем, нам придется многократно повторять одни и те же два параметра "info" и "Order Preparation". Итак, давайте применим частичное приложение (с использованием Lodash), чтобы исправить эти два параметра.

const loggerStepInfo = _.partial(logMessage, "info", _, "Order Preparation");

_ используется Lodash для обозначения параметра «заполнитель», который (в данном случае) является единственным изменяемым. Однако мы могли бы установить несколько параметров-заполнителей, если бы захотели. Это одно из основных различий между каррированием и частичным применением. Результирующая функция не обязательно должна быть унарной.

Теперь давайте проверим нашу получившуюся функцию с помощью нескольких примеров вызовов:

loggerStepInfo("Doing something");
loggerStepInfo("Doing another thing on same step");
loggerStepInfo("Doing some last thing");

Результат должен быть примерно таким:

Вот полный код с живым примером:

Заключение

Работа с функциональным программированием - не серебряная пуля. Ваш код не станет (или не будет работать) лучше, если он будет написан с использованием функционального стиля. Так что вам решать, использовать ли какую-либо технику FP, которую вы видели здесь или в любом другом месте.

По моему скромному мнению, когда дело доходит до разработки и реализации сложных алгоритмов или выполнения сложных вычислений, не требующих доступа к внешним ресурсам (файлам, базам данных, API), мне нравится делать это функционально и декларативно. Потому что я могу легко провести модульное тестирование и все разделить. Но если мне придется интенсивно иметь дело с внешними ресурсами, я буду придерживаться хорошо известной императивной формы. Опять же, это только я! У вас может быть другая мысль!

Я думаю, что ключевым элементом здесь является отделение нечистых частей вашего проекта от чистых. Так что вы можете легко отделить свой код и применить методы FP, когда сочтете это удобным.

В этой статье я не упомянул другие концепции, связанные с FP, такие как оптимизация рекурсии, монады, функторы и т. Д. Я нашел их достаточно продвинутыми для понимания и с несколькими практическими примерами использования, поэтому я решил пропустить их. Если вам все еще интересно, я предлагаю вам (снова) прочитать книгу Федерико Кереки Освоение функционального программирования на JavaScript.

Удачи!