Что такое 'Кюринг'?
Я встречал упоминания о каррированных функциях в нескольких статьях и блогах, но не могу найти хорошее объяснение (или хотя бы такое, которое действительно имеет смысл!).
5 ответ(ов)
Куррирование — это когда функция, принимающая несколько аргументов, разбивается на ряд функций, каждая из которых принимает только один аргумент. Вот пример на JavaScript:
function add(a, b) {
return a + b;
}
add(3, 4); // возвращает 7
Это функция, которая принимает два аргумента, a
и b
, и возвращает их сумму. Теперь мы применим куррирование к этой функции:
function add(a) {
return function(b) {
return a + b;
}
}
Теперь это функция, которая принимает один аргумент a
и возвращает функцию, принимающую другой аргумент b
. Эта внутренняя функция возвращает сумму a
и b
.
add(3)(4); // возвращает 7
var add3 = add(3); // возвращает функцию
add3(4); // возвращает 7
- В первом случае возвращается
7
, как и в случае вызоваadd(3, 4)
. - Во втором случае мы определяем новую функцию
add3
, которая будет добавлять3
к своему аргументу. Это то, что некоторые могут назвать замыканием. - В третьем случае мы используем операцию
add3
, чтобы добавить3
к4
, снова получая в результате7
.
Это может быть способ использовать функции для создания других функций.
В JavaScript:
let add = function(x) {
return function(y) {
return x + y;
};
};
Это позволяет нам вызывать её так:
let addTen = add(10);
Когда это выполняется, 10
передаётся как x
:
let add = function(10) {
return function(y) {
return 10 + y;
};
};
Что означает, что мы получаем эту функцию:
function(y) { return 10 + y; };
Таким образом, когда вы вызываете
addTen();
на самом деле вы вызываете:
function(y) { return 10 + y; };
Так что если вы сделаете это:
addTen(4);
это будет то же самое, что:
function(4) { return 10 + 4; } // 14
Таким образом, наша addTen()
всегда добавляет десять ко всему, что мы передаем. Мы можем создать подобные функции таким же образом:
let addTwo = add(2); // addTwo(); добавит два ко всему, что вы передадите
let addSeventy = add(70); // ... и так далее...
Теперь очевидный вопрос: зачем вообще это делать? Это превращает ту, что была жадной операцией x + y
, в ту, которую можно выполнять «лениво», что позволяет нам делать как минимум две вещи:
- кэшировать дорогие операции
- достигать абстракций в функциональной парадигме.
Представьте, что наша каррифицированная функция выглядит так:
let doTheHardStuff = function(x) {
let z = doSomethingComputationallyExpensive(x);
return function (y) {
return z + y;
}
}
Мы могли бы вызвать эту функцию один раз, а затем передать результат для использования в разных местах, что означает, что мы выполняем трудоёмкие вычисления только один раз:
let finishTheJob = doTheHardStuff(10);
finishTheJob(20);
finishTheJob(30);
Мы можем получить абстракции аналогичным образом.
Кэрри́нг — это преобразование, которое можно применить к функциям, позволяющее им принимать на одну аргумент меньше, чем ранее.
Например, в F# можно определить функцию так:
let f x y z = x + y + z
Здесь функция f
принимает параметры x
, y
и z
и суммирует их, так что:
f 1 2 3
Возвращает 6.
Согласно нашему определению, мы можем определить функцию curry
для функции f
следующим образом:
let curry f = fun x -> f x
Где fun x -> f x
— это лямбда-функция, соответствующая x => f(x)
в C#. Эта функция принимает функцию, которую вы хотите закэррить, и возвращает новую функцию, которая принимает единственный аргумент и возвращает указанную функцию с первым аргументом, установленным в значение данного аргумента.
Используя наш предыдущий пример, мы можем получить каррированную версию f
так:
let curryf = curry f
Затем мы можем сделать следующее:
let f1 = curryf 1
Это даст нам функцию f1
, которая эквивалентна f1 y z = 1 + y + z
. Это означает, что мы можем сделать следующее:
f1 2 3
Что вернёт 6.
Этот процесс часто путают с "частичным применением функции", которое можно определить так:
let papply f x = f x
Хотя мы можем расширить это на большее количество параметров, т.е.:
let papply2 f x y = f x y
let papply3 f x y z = f x y z
Частичное применение берёт функцию и один или несколько аргументов и возвращает функцию, которая требует на один или несколько параметров меньше. Как показывают предыдущие два примера, это реализуется напрямую в стандартном определении функции F#, так что мы можем достичь предыдущего результата следующим образом:
let f1 = f 1
f1 2 3
Что вернёт 6.
В заключение:
Разница между кэррингом и частичным применением функции заключается в том, что:
Кэрринг берёт функцию и предоставляет новую функцию, принимающую единственный аргумент и возвращающую указанную функцию с первым аргументом, установленным в этот аргумент. Это позволяет нам представлять функции с несколькими параметрами как последовательность функций с единственным аргументом. Пример:
let f x y z = x + y + z
let curryf = curry f
let f1 = curryf 1
let f2 = curryf 2
f1 2 3
6
f2 1 3
6
Частичное применение функции более прямолинейно — оно берёт функцию и один или несколько аргументов и возвращает функцию с первыми n
аргументами, установленными в указанные n
аргументы. Пример:
let f x y z = x + y + z
let f1 = f 1
let f2 = f 2
f1 2 3
6
f2 1 3
6
Курированная функция — это функция с несколькими аргументами, переписанная таким образом, что она принимает первый аргумент и возвращает новую функцию, которая принимает второй аргумент, и так далее. Это позволяет частично применять некоторые из исходных аргументов функции с несколькими аргументами. То есть, вы можете вызвать функцию с частью ее аргументов, и она вернет новую функцию, в которую можно будет передать оставшиеся аргументы. Такой подход часто используется для создания более гибких и переиспользуемых функций.
Куррирование (currying) — это преобразование функции с N аргументами в N функций, каждая из которых принимает один аргумент. Арность
(arity) функции — это количество аргументов, которое она требует.
Вот формальное определение:
curry(f) :: (a, b, c) -> f(a) -> f(b) -> f(c)
Рассмотрим реальный пример, который имеет смысл:
Предположим, вы пришли в банкомат, чтобы снять деньги. Вы вводите карту, вводите PIN-код, делаете выбор и нажимаете "ввод", чтобы подтвердить сумму вместе с запросом.
Вот обычная функция для снятия денег:
const withdraw = (cardInfo, pinNumber, request) => {
// обрабатываем запрос
return request.amount;
}
В этой реализации функция ожидает, что мы введем все аргументы сразу. Мы должны сначала вставить карту, ввести PIN, а затем сделать запрос, прежде чем функция будет выполнена. Если на каком-то из этих этапов возникнет ошибка, вы узнаете об этом только после ввода всех аргументов.
С помощью куррированной функции мы можем создать функции более высокой арности, чистые и простые. Чистые функции помогут нам легче отлаживать код.
Вот как будет выглядеть банкомат с куррированной функцией:
const withdraw = (cardInfo) => (pinNumber) => (request) => request.amount;
В этом случае банкомат принимает карту на вход и возвращает функцию, ожидающую PIN-код. Эта функция, в свою очередь, возвращает функцию, которая принимает объект запроса, и после успешной обработки вы получаете запрашиваемую сумму. На каждом шаге, если возникла ошибка, вы сможете легко определить, что пошло не так. Например, если вы вставили карту и получили ошибку, вы знаете, что это связано либо с картой, либо с банкоматом, но не с PIN-кодом. Если же вы ввели PIN-код, и он не был принят, вы знаете, что допустили ошибку при вводе PIN-кода. Таким образом, отладка ошибок становится более предсказуемой.
Кроме того, каждая из функций здесь является повторно используемой, так что вы можете применять эти же функции в различных частях вашего проекта.
Есть ли в JavaScript метод, аналогичный "range()", для генерации диапазона в заданных границах?
Функция map для объектов (вместо массивов)
Как перенаправить на другую веб-страницу?
Как проверить, содержит ли массив строку в TypeScript?
Ссылка и выполнение внешнего JavaScript-файла, размещенного на GitHub