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

Множество языков приняли такую стилистику в работе с кодом. Самые популярные из них это: JavaScript, Haskell, Clojure, Erlang и Scala.

Возможностью передавать и возвращать функции породила много других концепций:

  • Чистые функции
  • Каррирование
  • Функции высшего порядка

Что такое каррирование?

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

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

Каррирование - это процесс превращения функции с множественной арностью в функцию с меньшей арностью.

Обратите внимание, что термин арность относится напрямую к количеству аргументов которое берет функция. Для примера,

function fn(a, b) {
    //...
}

function _fn(a, b, c) {
    //...
}

У функции fn два аргумента (2х арная функция), а _fn имеет три аргумента (3х арная функция).

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

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

function multiply(a, b, c) {
    return a * b * c;
}

Функция берёт три аргумента, умножает числа и отдаёт результат.

multiply(1,2,3);  // 6

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

function multiply(a) {
    return (b) => {
        return (c) => {
            return a * b * c;
        }
    }
}

console.log(multiply(1)(2)(3));  // 6

Мы превратили функцию multiply(1,2,3) в несколько вызовов других вызовов multiply(1)(2)(3).

Просто напросто одна простая функция была переделана в серию последовательных функций. Чтобы получить результат умножения трёх чисел 1, 2 и 3, числа передаются одно за другим, каждое число передаётся следующей функции для запуска.

Мы могли бы разделить multiply(1)(2)(3) на несколько функций, чтобы вы лучше поняли процесс:

let mul1 = multiply(1);
let mul2 = mul1(2);
let result = mul2(3);

console.log(result);  // 6

Давайте рассмотрим всё по последовательности. Мы передали 1 функции multiply:

let mul1 = multiply(1);

Она отдаёт функцию:

return (b) => {
    return (c) => {
        return a * b * c
    }
}

Теперь mul1 это функция с аргументом b.

Далее мы берём функцию mul1, передавая ей 2:

let mul2 = mul1(2);

Mul1 отдаст третью функцию:

return (c) => {
    return a * b * c
}

Отданная функция теперь хранится в переменной mul2.

По-сути mul2 будет:

mul2 = (c) => {
    return a * b * c
}

Где mul2 вызывается с 3 как параметром,

const result = mul2(3);

Что делает вычисления вместе с параметрами, переданными ранее: a=1, b=2 и получаем 6.

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

Каррирование создаёт вложенные функции в соответствии с числом аргументов в ней. Каждая функция тут получает свой аргумент. Если нет аргумента, то нет и каррирования.


А каррирование действительно полезно?

Для примера у вас есть магазин и вы хотите дать 10% скидку вашим любимым клиентам:

function discount(price, discount) {
    return price * discount
}

Когда один из таких клиентов что-нибудь покупает на 500$:

const price = discount(500, 0.10); // $50 
// $500  - $50 = $450

Тут вы понимаете, что этот процесс будет происходить ежедневно:

const price = discount(1500, 0.10); // $150
// $1,500 - $150 = $1,350

const price = discount(2000, 0.10); // $200
// $2,000 - $200 = $1,800

const price = discount(50, 0.10); // $5
// $50 - $5 = $45

const price = discount(5000, 0.10); // $500
// $5,000 - $500 = $4,500

const price = discount(300, 0.10); // $30
// $300 - $30 = $270

Мы можем просто каррировать функцию скидки, чтобы нам не пришлось всегда добавлять 0.10.

function discount(discount) {
    return (price) => {
        return price * discount;
    }
}

const tenPercentDiscount = discount(0.1);

Теперь нам нужна только цена для расчета стоимости со скидкой:

tenPercentDiscount(500); // $50
// $500 - $50 = $450

И снова, так получается, что некоторые клиенты уж очень нам важны, важнее других. И мы хотим им дать скидку уже в 20%.

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

const twentyPercentDiscount = discount(0.2);

Мы делаем новую функцию для наших очень важных клиентов, вызывая функцию каррирования со значением 0.2, что есть 20%.

Отдаваемая функция twentyPercentDiscount будет использоваться для расчета скидок у таких важных клиентов:

twentyPercentDiscount(500); // 100
// $500 - $100 = $400

twentyPercentDiscount(5000); // 1000
// $5,000 - $1,000 = $4,000

twentyPercentDiscount(1000000); // 200000
// $1,000,000 - $200,000 = $600,000