Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Частичное применение JS: Функциональное программирование

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

const sum = (a, b, c) => a + b + c;
sum(4, 3, 1); // 8
sum(1, 1, 1); // 3

Частичное применение позволяет на основе старой функции создать новую, которая "частично применена". Для начала вспомним, что такое применение функции. Математики никогда не говорят, что функция вызывается с некоторыми аргументами — вместо этого они говорят, что функция была применена к этим аргументам. В примере выше функция sum была применена к трем аргументам 4, 3 и 1. Такое применение можно назвать полным, то есть в функцию было передано столько аргументов сколько и ожидалось. Здесь возникает вопрос, а можно было по-другому? Да, можно.

Частичное применение, техника которую проще всего объяснить в отрыве от языков программирования, на языке близком к математике (для простоты назову его Ha). Представьте, что наша функция имеет вот такое определение:

-- Слева имя функции, затем идет список параметров,
-- разделенных пробелами, и после знака "=>" — тело функции
sum a b c => a + b + c

А ее вызов выглядит так:

-- Скобок и запятых нет, но это равносильно sum(4, 3, 1) в js
sum 4 3 1 -- 8
sum 1 1 1 -- 3

Ha — необычный язык. Если внутри него вызвать функцию с неполным набором параметров, то в отличие от js он не вызовет саму функцию (или не упадет с ошибкой как во многих других языках). Он вернет новую функцию, которая "частично применена".

-- sum2 новая функция, полученная частичным применением функции sum к числу четыре
-- Применение "частичное", потому что в функцию sum передается только один параметр, а не три
sum2 = sum 4
sum2 1 1 -- 6
sum2 3 2 -- 9

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

sum3 = sum 1 1
-- sum3 принимает только один аргумент
sum3 2 -- 4
sum3 1 -- 3

Арифметика аргументов очень простая. Если исходная функция принимала 3 параметра, то после частичного применения одного параметра, новая функция будет принимать на вход два параметра (2 + 1 = 3), если частично применились два параметра, то новая функция принимает на вход 1 параметр. А можно ли частично применить три параметра для функции, которая принимает на вход 3 параметра? Конечно нет, ведь это будет обычный вызов функции со всеми параметрами. Довольно несложно догадаться как выглядело бы определение функций sum2 и sum3 если бы мы их описали явно:

sum a b c => a + b + c
sum2 b c => 4 + b + c
sum3 c => 1 + 1 + c

Ha настолько необычный язык, что позволяет частично применять то, что уже было частично применено:

-- Создаем первую функцию с 3 - 1 аргументами
sum4 = sum 2
-- Создаем вторую функцию с 2 - 1 аргументами
sum5 = sum4 3
-- Вызываем то что получилось. В реальности вычисляется 2 + 3 + 1
sum5 1 -- 6
sum5 4 -- 9

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

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

// Берем исходную функцию sum и отдаем ее в функцию partialApply
const sumWithFour = partialApply(sum, 4);
sumWithFour(3, 1); // 8
sumWithFour(10, 3); // 17

const sumWithFourAndTwo = partialApply(sumWithFour, 2);
sumWithFourAndTwo(3); // 9

Функция partialApply принимает на вход исходную функцию и параметры которые нужно применить, а возвращает новую, частично примененную функцию.

Вот как может выглядеть реализация partialApply для функций от трех аргументов (таких как sum):

const partialApply = (fn, arg1) => (arg2, arg3) => fn(arg1, arg2, arg3);

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

const salary1 = getAverageSalary('programmer', 'spain');
const salary2 = getAverageSalary('programmer', 'russia');
const salary3 = getAverageSalary('programmer', 'usa');

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

const job = 'programmer'
const salary1 = getAverageSalary(job, 'spain');
const salary2 = getAverageSalary(job, 'russia');
const salary3 = getAverageSalary(job, 'usa');

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

const getProgrammersSalaryByCountry = partialApply(getAverageSalary, 'programmer');

const salary1 = getProgrammersSalaryByCountry('spain');
const salary2 = getProgrammersSalaryByCountry('russia');
const salary3 = getProgrammersSalaryByCountry('usa');

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

Тело функции getProgrammersSalaryByCountry выглядит предсказуемо. Внутри вызывается исходная функция с подставленным в аргумент значением.

const getProgrammersSalaryByCountry = country => getAverageSalary('programmer', country);

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

const partialApply = (fn, arg1) => (arg2) => fn(arg1, arg2);

То есть функция partialApply возвращает (генерирует) функцию, которая внутри себя замыкает два параметра: fn и arg1.

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


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

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

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»