Эта история начинается, как и все лучшие истории, с вопроса на Stack Overflow (до того, как был опубликован ответ, что, как я теперь вижу, выбивает из колеи то, что я здесь пишу 🤷♂️ Я все равно собираюсь это сделать). Вопрос был «Как приготовить бесконечное карри?» или что-то в этом роде. Я люблю карри*поэтому я решила посмотреть.
Один из комментариев к вопросу таков:
…в прикладном языке, таком как JavaScript, это не совсем «карри».
И я подумал: если это не карри, то что же это такое? В статье «Currying» в Википедии есть такой комментарий:
Currying связан с частичным применением, но не то же самое, что и частичное применение.
Поэтому я посмотрел, что такое частичное применение.
Частичное применение — это когда у вас есть унарная функция (функция, которая принимает только один аргумент), и она возвращает другую унарную функцию, и так далее, пока у вас не будет достаточно аргументов. Например:
function add3Numbers(x,y,z)
{
return x + y + z;
}
Это функция, которая принимает 3 аргумента (x, y и z). Чтобы сделать ее функцией частичного применения, или, скорее, функциями, мы можем сделать следующее:
function add3Numbers(x)
{
return function(y)
{
return function(z)
{
return x+y+z;
}
}
}
Которая может быть вызвана следующим образом:
add3Numbers(11)(8)(2022);
Но также
const add8yz = add3numbers(8);
add8yz(4)(5)
Эти игрушечные примеры не демонстрируют возможности частичного применения, а лишь дают вам представление о том, как оно работает. Его основное назначение — облегчить объединение функций. Считайте это сродни цепочке методов в объектно-ориентированных языках, например:
' abcdefghijklmnopqrstuvwxyz '.trim().toUpperCase().slice(3).replace('Q', 'q');
Где каждый метод возвращает объект того же типа, что и вызывающий объект.
Что если, размышлял я, я захочу превратить обычную функцию в функцию, способную к частичному применению? Можно ли это сделать?
Ответ, который я создал, звучит как «вроде того». Я использую свойство .length
функции, что означает, что она не будет работать для:
- Вариативных функций (например,
console.log
). Это происходит потому, что количество параметров не определено. - Функций, использующих остальные параметры (например,
function f(a, b, ...c){}
)..length
не учитывает остальные параметры. - Функции с параметрами, имеющими значения по умолчанию (например,
function f(a, b=0){}
). В этом случае.length
игнорирует параметры со значениями по умолчанию.
Поэтому мое решение ограничено функциями, которые имеют явное количество параметров и не имеют значений по умолчанию для параметров.
Любая из следующих форм является допустимой:
const func1 = function(a,b,c){};
function func2(a,b,c){}
const func3 = (a,b,c) => {};
С чего мы начнем? Позвольте представить вам моего хорошего друга Proxy
.
С его помощью мы можем делать вид, что вызываем функцию, а на самом деле делать что-то другое.
Прокси нужен обработчик, чтобы совершить волшебство. Я представил частичные функции как связанный список:
const handler =
{
next: null,
apply: function(target, thisArg, argumentsList)
{
if(this.next)
{
const uc = new unaryCall(argumentsList[0]);
uc.args = thisArg.args.concat(argumentsList);
return (new Proxy(unaryCall, this.next)).bind(uc);
}
else
{
const args = thisArg.args.concat(argumentsList);
return initialFunction(...args);
}
}
};
Функция apply
— это то, что заменяет вызов обычной функции. Вы можете заметить, что я не просто создаю прокси, но и привязываю его к объекту.
Я пытался понять, как мне сохранить аргументы, не перезаписывая их. В конце концов, я понял, что мне нужно как-то изменить созданный прокси, и пришел к выводу, что я могу установить thisArg
, привязав прокси.
Чтобы не попасть в какой-то предел рекурсии, каждый объект, привязанный к прокси, хранит аргументы для себя и предыдущих объектов. Я думаю, что это обеспечивает более быстрый доступ, но ценой того, что занимает гораздо больше места.
В конце, т.е. если нет следующего обработчика, то обработчик возвращает результат вызова исходной функции с накопленными аргументами.
Построение списка происходит в цикле. Я рассматривал возможность рекурсии, так как она выглядела бы более элегантно, но предел стека меньше, чем предел размера массива, поэтому я решил проявить осторожность:
let handlers = [];
for(let i = 0; i < initialFunction.length; i++)
{
const handler =
{
next: null,
apply: function(target, thisArg, argumentsList)
{
if(this.next)
{
const uc = new unaryCall(argumentsList[0]);
uc.args = thisArg.args.concat(argumentsList);
return (new Proxy(unaryCall, this.next)).bind(uc);
}
else
{
const args = thisArg.args.concat(argumentsList);
return initialFunction(...args);
}
}
};
if(handlers.length > 0)
{
const last = handlers[handlers.length-1];
last.next = handler;
}
handlers.push(handler);
}
const firstUnary = new unaryCall(null);
firstPart = (new Proxy(unaryCall, handlers[0])).bind(firstUnary);
Создание unaryCall
на самом деле не нуждалось в параметре, но я поместил его туда, чтобы удовлетворить все, что происходит в моем мозгу.
Вы можете видеть в верхней части цикла, что я перебираю все параметры функции, используя длину функции. На самом деле вы не можете получить доступ к параметрам в определении, так что это очень близко.
Это основная часть. Вы можете увидеть всю функцию в этом gist
Сообщите мне о своих мыслях и любых вопросах в комментариях ниже!
* К сожалению, я больше не могу наслаждаться острым карри из-за недавно приобретенного заболевания, так что мое наслаждение в эти дни больше гипотетическое.