В последнее время я увлекся функциональным программированием благодаря исключительной серии Программное обеспечение для составления Эрика Эллиота, которую необходимо прочитать, если вы пишете JavaScript. В какой-то момент он упомянул currying, инструмент, который позволяет вам частично применять функцию, то есть вам не нужно указывать ее аргументы сразу.

Итак, если у вас есть

greet = (greeting, first, last) => `${greeting}, ${first} ${last}`
greet('Hello', 'John', 'Doe') // Hello, John Doe

Карри, и вы получите

curriedGreet = curry(greet)
curriedGreet('Hello')('John')('Doe') // Hello, John Doe
curriedGreet('Hello', 'John')('Doe') // Hello, John Doe

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

Немного подробнее:

// greet requires 3 params: (greeting, first, last)
// these all return a function looking for (first, last)
curriedGreet('Hello')
curriedGreet('Hello')()
curriedGreet()('Hello')()()
// these all return a function looking for (last)
curriedGreet('Hello')('John')
curriedGreet('Hello', 'John')
curriedGreet('Hello')()('John')()
// these return a greeting, since all 3 params were honored
curriedGreet('Hello')('John')('Doe')
curriedGreet('Hello', 'John', 'Doe')
curriedGreet('Hello', 'John')()()('Doe')

Как показано выше, каррированную функцию можно вызывать бесконечно без параметров, и она всегда будет возвращать функцию, ожидающую остальных параметров. # Лояльность

Но как это возможно?

Г-н Эллиот поделился curry реализацией в этой статье. Вот код (или, как он метко назвал его, магическое заклинание):

const curry = (
  f, arr = []
) => (...args) => (
  a => a.length === f.length ?
    f(...a) :
    curry(f, a)
)([...arr, ...args]);

Эмм… 😐

Давайте расширим это краткое произведение искусства и оценим его вместе

curry = (originalFunction, initialParams = []) => {
    debugger;
    return (...nextParams) => {
        debugger;
        const curriedFunction = (params) => {
            debugger;
            if (params.length === originalFunction.length) {
                return originalFunction(...params);
            }
            return curry(originalFunction, params);
        };
        return curriedFunction([...initialParams, ...nextParams]);
    };
};

Я добавил несколько debugger операторов, чтобы приостановить код в важных местах. Я настоятельно рекомендую современный браузер для такой отладки, потому что вы можете легко проверить соответствующие параметры в разных точках.

См. Любую из этих ссылок, если я несу чушь.

Быстрые и грязные шаги для доступа к DevTools (могут работать не во всех случаях)

  1. Откройте вкладку в вашем браузере
  2. Щелкните правой кнопкой мыши в любом месте страницы и выберите «Проверить элемент».
  3. должна выскочить консоль DevTools. Перейдите на вкладку «консоль».

Хорошо, давай сделаем это!

Вставьте greet и curry в консоль. Затем введите curriedGreet = curry(greet) и начните безумие.

Мы останавливаемся на строке 2. Проверяя наши два параметра, мы видим, что originalFunction greet и initialParams по умолчанию установлен пустой массив, потому что мы его не предоставили. Перейдите к следующей точке останова и, подождите… вот и все.

Ага! curry(greet) просто возвращает новую функцию, которая ожидает еще 3 параметра. Введите curriedGreet в консоли, чтобы узнать, о чем я говорю.

Когда вы закончите играть с этим, давайте немного станем сумасшедшим и сделаем
sayHello = curriedGreet('Hello').

Теперь мы внутри функции, определенной в строке 4. Прежде чем продолжить, введите originalFunction и initialParams в консоли. Заметили, что мы все еще можем получить доступ к этим двум параметрам, даже если находимся в совершенно новой функции? Это связано с тем, что функции, возвращаемые родительскими функциями, имеют родительскую область видимости.

Даже если родительская функция передана, они оставляют параметры для использования детьми.

Вроде как наследование (в реальном смысле, а не ООП). curry изначально были заданы originalFunction и initialParams, а затем возвращена дочерняя функция. Эти две переменные еще не были собраны мусором, потому что, возможно, Function Jr. хочет их использовать. Если он этого не сделает, тогда эта область будет очищена, потому что, когда на вас никто не ссылается, вы действительно умираете.

Хорошо, вернемся к строке 4…

Осмотрите nextParams и убедитесь, что это ['Hello']… массив? Но я думал, мы сказали curriedGreet(‘Hello’), а не curriedGreet(['Hello'])!

Правильно: мы вызвали curriedGreet с 'Hello', но благодаря остальному синтаксису мы превратились 'Hello' в ['Hello'].

Y THO ?!

curry - это общая функция, которой может быть предоставлено 1, 10 или 10 000 000 параметров, поэтому ей нужен способ ссылки на все из них. Использование остального синтаксиса, подобного этому, захватывает каждый параметр в одном массиве, что значительно упрощает работу curry.

Перейдем к следующему debugger оператору.

Сейчас мы на линии 6, но подождите.

Возможно, вы заметили, что строка 12 на самом деле идет перед оператором debugger в строке 6. Если нет, присмотритесь. Наша программа определяет функцию с именем curriedFunction в строке 5, использует ее в строке 12, и затем мы нажимаем этот оператор debugger в строке 6. А что вызывает curriedFunction?

[…initialParams, …nextParams]

Юуууп. Посмотрите на params в строке 5, и вы увидите ['Hello']. И initialParams, и nextParams были массивами, поэтому мы сгладили и объединили их в один массив с помощью удобного оператора spread (тот же синтаксис, что и у rest, но вместо этого он расширяется конденсации).

Если хотите, я написал статью, подробно описывающую распространение и Object.assign: https://medium.com/@ybzadough/how-do-object-assign-and -распространенная-собственно-работа-169b53275cb

Вот где происходит хорошее.

В строке 7 говорится: «Если params и originalFunction имеют одинаковую длину, вызовите greet с нашими параметрами, и все готово». Что напоминает мне…

У функций JavaScript тоже есть длина

Вот как curry творит чудеса! Таким образом он решает, запрашивать ли дополнительные параметры. В JavaScript свойство .length функции сообщает вам сколько аргументов она ожидает.

greet.length // 3
((a, b) => {}).length // 2
((a) => {}).length // 1

Если предоставленные и ожидаемые параметры совпадают, все в порядке, просто передайте их исходной функции и завершите работу!

Это балерина 🏀

Но в нашем случае параметры и длина функции не одинаковы. Мы предоставили только ‘Hello’, поэтому params.length равно 1, а originalFunction.length равно 3, потому что greet ожидает 3 параметра: greeting, first, last.

Что будет дальше?

Поскольку этот оператор if оценивается как false, код перейдет к строке 10 и повторно вызовет нашу главную функцию curry. Он повторно принимает greet и на этот раз 'Hello', и снова начинает безумие.

Это рекурсия, друзья мои.

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

Теперь вы вернулись к строке 2 с теми же параметрами, что и раньше, за исключением того, что initialParams на этот раз ['Hello']. Пропустите снова, чтобы выйти из цикла. Введите в консоль нашу новую переменную sayHello. Это еще одна функция, ожидающая большего количества параметров, но мы становимся теплее ...

Давайте разогреемся с sayHelloToJohn = sayHello('John').

Мы снова внутри четвертой строки, а nextParams это ['John']. Перейдите к следующему отладчику в строке 6 и проверьте params: это ['Hello', 'John']! 🙀

Почему, почему, почему?

Потому что помните, в строке 12 написано: «Привет, curriedFunction, он дал мне 'Hello' в прошлый раз и ‘John’ в этот раз. Возьмите их обоих в этот массив [...initialParams, ...nextParams] ».

Теперь curriedFunction снова сравнивает length этих params с originalFunction, а с 2 < 3 мы переходим к строке 10 и снова вызываем curry! И, конечно же, мы передаем greet и наши 2 параметра ['Hello', 'John']

Мы так близко, давайте закончим и получим полное приветствие!

sayHelloToJohnDoe = sayHelloToJohn('Doe')

Думаю, мы знаем, что будет дальше.

Дело сделано.

greet получил свои параметры, curry перестал зацикливаться, и мы получили наше приветствие: Hello, John Doe.

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

curriedGreet('Hello', 'John', 'Doe')
curriedGreet('Hello', 'John')('Doe')
curriedGreet()()('Hello')()('John')()()()()('Doe')

Большое спасибо Эрику Эллиотту за то, что представил мне это, и даже больше за то, что вы оценили curry вместе со мной. До скорого!

Береги себя,
Язид Бзадоу