Каррирование и частичное применение в JavaScript
Каррирование и частичное применение — мощные техники функционального программирования, которые улучшают модульность и переиспользуемость JavaScript-кода.
Каррирование и частичное применение — это мощные техники функционального программирования, которые могут значительно улучшить модульность и переиспользуемость вашего JavaScript-кода. Обе техники опираются на замыкания — способность внутренней функции запоминать переменные из внешней функции, которая её создала — поэтому возвращённая функция сохраняет уже переданные аргументы.
В этой статье рассматривается, что такое каррирование и почему оно важно, как написать переиспользуемый вспомогательный метод curry, чем частичное применение отличается от каррирования, реальные сценарии использования, а также лучшие практики и подводные камни, о которых следует помнить.
Понимание каррирования
Каррирование — это техника, при которой функция преобразуется в последовательность функций, каждая из которых принимает один аргумент. Она позволяет разбить функцию, принимающую несколько аргументов, на серию унарных (однoаргументных) функций.
Пример каррирования
Рассмотрим простую функцию, которая складывает три числа:
function add(a, b, c) {
return a + b + c;
}Мы можем преобразовать эту функцию в каррированную версию:
Пояснение:
- Функция
addпринимает три аргумента и возвращает их сумму. - Функция
curryAdd— это каррированная версияadd. Она принимает один аргументaи возвращает другую функцию, принимающуюb. Эта вторая функция возвращает ещё одну функцию, принимающуюc. Самая внутренняя функция возвращает суммуa,bиc.
Преимущества каррирования
- Переиспользуемость: Каррированные функции позволяют создавать новые функции, фиксируя часть аргументов. Это повышает переиспользуемость, поскольку позволяет легко создавать специализированные версии функции без дублирования кода.
Здесь curriedAdd — каррированная версия функции add. Она позволяет создавать специализированные функции, например add5, фиксируя первый аргумент (a) равным 5. Это способствует повторному использованию кода, так как можно создавать множество специализированных функций без повторения логики сложения.
- Композиция функций: Каррирование упрощает композицию функций. Композиция функций — это процесс объединения двух или более функций для получения новой функции.
В этом примере мы компонуем функции multiply и addOne с помощью каррирования. Краткая форма a => b => a * b опирается на стрелочные функции, которые делают вложенные однoаргументные функции легко читаемыми. Благодаря каррированию этих функций мы можем легко создать новую функцию addOneThenMultiplyBy5, которая сначала прибавляет 1 к входному значению (x), а затем умножает результат на 5. Это демонстрирует, как каррирование облегчает композицию функций, упрощая создание новых функций на основе существующих.
Реализация каррирования в JavaScript
Мы можем создать служебную функцию для каррирования любой функции. Вот реализация обобщённой функции каррирования:
Пояснение:
-
Функция каррирования (
curry):- Функция
curryпринимает другую функциюfnв качестве аргумента. - Она возвращает новую функцию с именем
curried. - Эта функция
curriedпринимает произвольное количество аргументов с помощью синтаксиса остаточных параметров (...args).
- Функция
-
Каррированная функция (
curried):- Внутри
curriedпроверяется, не меньше ли количество переданных аргументов (args.length), чем количество аргументов, ожидаемых исходной функциейfn(fn.length). - Если аргументов достаточно, вызывается исходная функция
fnс этими аргументами черезfn.apply(this, args). - Если аргументов недостаточно, возвращается новая функция, принимающая дополнительные аргументы (
nextArgs) с помощью оператора расширения (...nextArgs). - Эта новая функция рекурсивно вызывает
curriedс объединёнными аргументами (args.concat(nextArgs)), гарантируя сбор всех аргументов перед вызовом исходной функцииfn.
Примечание:
fn.lengthучитывает только параметры без значений по умолчанию и игнорирует остаточные параметры. - Внутри
-
Пример использования:
- Мы определяем функцию
multiply, которая принимает три аргумента и возвращает их произведение. - Мы создаём каррированную версию функции
multiply, передавая её в функциюcurry, которая возвращает новую функциюcurriedMultiply. - Теперь
curriedMultiplyможно вызывать с одним, двумя или тремя аргументами. - При каждом вызове
curriedMultiplyс аргументом или аргументами он возвращает новую функцию, пока все аргументы не будут собраны, после чего возвращается результат их произведения.
- Мы определяем функцию
Частичное применение
Частичное применение — это техника, при которой создаётся новая функция путём предварительного заполнения некоторых аргументов исходной функции. Это особенно полезно для создания специализированных функций.
Пример частичного применения
Рассмотрим следующую функцию форматирования сообщения:
function formatMessage(greeting, name) {
return `${greeting}, ${name}!`;
}Мы можем создать частично применённую функцию:
Пояснение:
- Функция
formatMessageпринимает два аргумента —greetingиname— и возвращает отформатированное сообщение. - Функция
partialпринимает функциюfnи некоторые предустановленные аргументы (...presetArgs). Она возвращает новую функцию, принимающую оставшиеся аргументы (...laterArgs). - При вызове новой функции она объединяет
presetArgsиlaterArgsи вызывает исходную функциюfnс этими аргументами. - С помощью
partialмы создаёмgreetHello— функцию, которая всегда использует "Hello" в качестве приветствия. При вызове с именем она возвращает полное сообщение.
Преимущества частичного применения
-
Упрощение: Создание более простых функций из сложных
Предположим, у нас есть функция, которая вычисляет итоговую цену товара после применения скидки и налога.
function calculateFinalPrice(price, discount, tax) {
return price - (price * discount) + (price * tax);
}Эта функция требует три аргумента, что делает её несколько неудобной для повторного использования, если ставки скидки и налога часто одинаковы. С помощью частичного применения мы можем упростить это.
В приведённом примере applyDiscountAndTax — это частично применённая функция, которая предустанавливает значения discount и tax. Это упрощает расчёт итоговой цены для различных товаров без повторного указания ставок скидки и налога.
-
Повторное использование кода: Переиспользование общей логики функции с разными предустановленными аргументами
Представьте, что у нас есть функция, которая записывает сообщения с разными уровнями серьёзности.
function logMessage(level, message) {
console.log(`[${level}] ${message}`);
}С помощью частичного применения мы можем создать переиспользуемые функции для разных уровней журнала.
Здесь createLogger — частично применённая функция, которая устанавливает аргумент level. Функции infoLogger и errorLogger теперь можно использовать для записи сообщений с предустановленными уровнями, повторно используя общую логику logMessage.
- Улучшение читаемости: Делает код более читаемым за счёт разбиения сложных функций Рассмотрим функцию, которая форматирует даты в разных стилях.
function formatDate(date, format) {
const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
if (format === 'US') {
options.month = 'long';
} else if (format === 'EU') {
options.day = 'numeric';
options.month = 'numeric';
}
return new Date(date).toLocaleDateString(undefined, options);
}Используя частичное применение, мы можем создать более читаемые функции для разных форматов дат.
createDateFormatter частично применяет аргумент format, в результате чего получаются конкретные функции для форматов дат US и EU. Такое разбиение делает код более читаемым и понятным, поскольку каждая функция-форматтер предназначена для определённого формата.
Эти примеры иллюстрируют, как частичное применение в JavaScript может упростить сложные функции, повысить переиспользуемость кода и улучшить читаемость, делая код проще в управлении и понимании.
Каррирование vs. частичное применение
Эти две техники тесно связаны и нередко путаются, но они не одно и то же:
- Каррирование преобразует функцию из N аргументов в N вложенных функций, каждая из которых принимает ровно один аргумент. Полностью каррированная функция всегда вызывается по одному аргументу:
f(a)(b)(c). - Частичное применение фиксирует некоторые аргументы функции и возвращает новую функцию, принимающую оставшиеся в одном вызове:
g = partial(f, a), затемg(b, c).
Короче говоря: каррирование касается формы функции (цепочка унарных вызовов), тогда как частичное применение касается предварительного заполнения аргументов. Обратите внимание, что обобщённый вспомогательный метод curry, показанный выше, более гибок, чем строгое каррирование — он принимает аргументы группами (curriedMultiply(2, 3)(4)), тем самым стирая грань и фактически поддерживая частичное применение.
Лучшие практики использования каррирования и частичного применения
Сохраняйте функции чистыми
- Убедитесь, что каррированные и частично применённые функции остаются чистыми, без побочных эффектов. Это упрощает их понимание и тестирование.
Используйте там, где это уместно
- Применяйте каррирование и частичное применение там, где они органично подходят для решаемой задачи.
- Избегайте злоупотребления этими техниками, так как при неосторожном использовании они могут затруднить понимание кода.
Используйте композицию функций
- Комбинируйте каррированные функции для создания более сложной функциональности. Каррирование хорошо сочетается с композицией функций, что ведёт к более модульному коду.
Документирование и именование
- Правильно документируйте каррированные и частично применённые функции, указывая их предполагаемое использование.
- Используйте понятные и описательные имена функций, отражающие их назначение.
Остерегайтесь этих подводных камней
fn.lengthненадёжен для автоматического каррирования. Обобщённый вспомогательный методcurry, опирающийся наfn.length, будет некорректно работать с параметрами по умолчанию или остаточными параметрами, поскольку они не учитываются. Если функция их использует, передавайте ожидаемое арность явно, не полагаясь наfn.length.- Чрезмерное каррирование ухудшает читаемость. Длинная цепочка
f(a)(b)(c)(d)труднее для чтения, чем один хорошо названный вызов. Прибегайте к каррированию, когда оно действительно устраняет повторение. - Следите за
this. Стрелочные функции не имеют собственногоthis, поэтому каррированная цепочка, написанная со стрелочными функциями, не может использоваться как метод, зависящий от вызывающего объекта. Если вам нужен динамическийthis, см. привязку функций.
Если вы часто используете каррирование и частичное применение, рассмотрите возможность использования библиотеки Lodash, которая предоставляет удобные служебные функции, такие как _.curry и _.partial.
Сочетание с методами массивов
Каррирование можно эффективно сочетать с методами массивов, такими как map, filter и reduce, для создания лаконичного и выразительного кода.
Пояснение:
- Функция
curriedMultiplyиспользуется для созданияmultiplyByTwo— функции, умножающей свой аргумент на 2. - Массив
numbersпреобразуется с помощьюmap, применяяmultiplyByTwoк каждому элементу, что даёт новый массив удвоенных чисел.
Заключение
Каррирование и частичное применение в JavaScript предлагают мощные техники для упрощения композиции функций, повышения переиспользуемости кода и улучшения читаемости. Каррирование преобразует функции с несколькими аргументами в серию унарных функций, обеспечивая более гибкий и модульный код. Частичное применение позволяет предустанавливать аргументы функций, облегчая повторное использование кода и упрощение сложных функций. Используя эти концепции функционального программирования, разработчики могут писать более чистый, лаконичный и легко поддерживаемый JavaScript-код.