Javascript Прокси и частичное применение функций

Эта история начинается, как и все лучшие истории, с вопроса на 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 функции, что означает, что она не будет работать для:

  1. Вариативных функций (например, console.log). Это происходит потому, что количество параметров не определено.
  2. Функций, использующих остальные параметры (например, function f(a, b, ...c){}). .length не учитывает остальные параметры.
  3. Функции с параметрами, имеющими значения по умолчанию (например, 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

Сообщите мне о своих мыслях и любых вопросах в комментариях ниже!


* К сожалению, я больше не могу наслаждаться острым карри из-за недавно приобретенного заболевания, так что мое наслаждение в эти дни больше гипотетическое.

Оцените статью
devanswers.ru
Добавить комментарий