В последнее время я увлекся функциональным программированием благодаря исключительной серии Программное обеспечение для составления Эрика Эллиота, которую необходимо прочитать, если вы пишете 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
операторов, чтобы приостановить код в важных местах. Я настоятельно рекомендую современный браузер для такой отладки, потому что вы можете легко проверить соответствующие параметры в разных точках.
См. Любую из этих ссылок, если я несу чушь.
- Chrome: https://developer.chrome.com/devtools
- Firefox: https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Open_the_debugger
- Edge: https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide
- IE 11 (при необходимости): https://msdn.microsoft.com/en-us/library/bg182326(v=vs.85).aspx
Быстрые и грязные шаги для доступа к DevTools (могут работать не во всех случаях)
- Откройте вкладку в вашем браузере
- Щелкните правой кнопкой мыши в любом месте страницы и выберите «Проверить элемент».
- должна выскочить консоль 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
вместе со мной. До скорого!
Береги себя,
Язид Бзадоу