очень простой, очень простой

Когда я читаю medium Эрика Эллиотта о функциональной композиции, я очень запутался в его реализации функции 'curry', которая кажется простой имитацией функции карри lodash.js, и это в ES6.

const curry = fn => (…args) => fn.bind(null, …args);

Чтобы помочь другим разработчикам UI / UX понять, что происходит за этой строкой кода, я решил написать этот носитель и попытаться использовать несколько очень простых и интуитивно понятных, иногда наивных примеров, чтобы шаг за шагом проиллюстрировать базовую реализацию функции карри.

Давайте сначала проверим документ lodash.js, чтобы увидеть, что делает настоящий карри.

var abc = function(a, b, c) { return [a, b, c];};
var curried = _.curry(abc);
curried(1)(2)(3); // => [1, 2, 3]
curried(1, 2)(3); // => [1, 2, 3]
curried(1, 2, 3); // => [1, 2, 3]
// Curried with placeholders.
curried(1)(_, 3)(2); // => [1, 2, 3]

В моем понимании карри позволяет нам:

  1. Собирайте аргументы поэтапно в несколько вызовов функций вместо одного.
  2. Когда будет собрано достаточно аргументов, верните результат функции.

Чтобы лучше понять это, я проверил несколько примеров реализации в Интернете. Однако вместо того, чтобы начинать с окончательной реализации, я хотел бы иметь очень простое руководство, начинающееся с очень базового пример, как показано ниже.

Понимание функции fn - это начальная точка всего. По сути, эта функция работает как сборщик аргументов. Каждый раз, когда вызывается эта функция, она возвращает связанную функцию (fb) самой себя и привязывает предоставленные аргументы этой функции к той функция возврата. Аргументы будут предшествовать любым предоставленным аргументам позже, когда вызывается эта связанная функция. Таким образом, аргументы в каждом вызове будут постепенно собираться в массив.

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

Чтобы заставить его вести себя как функция карри, необходимо решить две проблемы:

  1. Вместо того, чтобы выводить аргументы в конце, передав их console.log, мы хотим передать собранные аргументы целевой функции, которой они нужны.
  2. numOfRequiredArguments не следует жестко запрограммировать, это должно быть количество аргументов, ожидаемых целевой функцией.

К счастью, функция Javascript имеет свойство под названием «длина», которое определяет количество аргументов, ожидаемых функцией. Таким образом, вместо жестко запрограммированного, мы всегда можем использовать это свойство для определения
количества требуемых аргументов. Вторая проблема решена!

А как насчет первой проблемы: поддерживать ссылку на целевую функцию?

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

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

Подход # 1

что делает magician функция: она принимает целевую функцию в качестве аргумента, а затем возвращает функцию «сборщик аргументов», такую ​​же, как fn, показанный выше. Единственное отличие состоит в том, что когда количество собранных аргументов совпадает с количеством обязательных аргументов целевой функции, она применяет эти собранные аргументы к этой целевой функции и возвращает рассчитанный результат. Этот подход решает первую проблему (ссылку на целевую функцию), сохраняя ее в замыкании, созданном magician.

Подход # 2

Этот подход делает еще один шаг вперед. Поскольку функция сборщика аргументов - это обычная функция, почему бы просто не использовать сам magician в качестве сборщика аргументов?

Обратите внимание на одно отличие в подходе №2. Поскольку magician принимает целевую функцию в качестве своего первого аргумента, собранные аргументы всегда будут включать эту функцию в качестве arguments [0]. В результате нам нужно обрезать этот первый аргумент при проверке общего количества допустимых аргументов.

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

Как вы можете видеть, функция карри, использованная Эриком Эллиоттом выше, аналогична подходу №1, но на самом деле это частичное применение (другая история).

const curry = fn => (…args) => fn.bind(null, …args);

выше показан карри, который возвращает «сборщик аргументов», который собирает аргументы только один раз и возвращает связанную целевую функцию.

Еще один шаг

Вышеупомянутый «волшебник» все еще не такой волшебный, как функция «карри» в lodash.js. Карри lodash позволяет использовать "_" в качестве заполнителя для входных аргументов.

curried(1)(_, 3)(2); // => [1, 2, 3], notice the placeholder '_'

Для достижения функциональности заполнителя существует неявное требование: нам нужно знать, какие аргументы предустановлены для связанной функции, а какие дополнительные аргументы предоставляются явно при вызове функции (давайте назовем их «добавленными» аргументами).

Это можно сделать, создав еще одно закрытие.

fn2 выше - это почти то же самое fn, которое работает как «сборщик аргументов». Однако вместо прямого возврата связанной функции fn2 возвращает промежуточную функцию helper. Функция helper не связана, и поэтому ее можно использовать для разделения «предустановленных» аргументов и «добавленных» аргументов.

Конечно, вместо объединения «предустановок» и «добавленных» [...preset, ...added] нам нужно внести некоторые изменения при их объединении. Нам нужно найти местоположение заполнителя в «предустановке» и заменить его действительным «добавленным» аргументом. Я не проверял, как это делает lodash, но ниже представлена ​​простая реализация для достижения аналогичной функциональности.

Строки с 15 по 24 - это логика, используемая для помещения «добавленных» аргументов в правильную позицию в массиве «предустановок»: либо заполнитель, либо конец предустановки. Эта позиция помечена как nextPos и инициализирована как 0 индекс.

Теперь magician3 работает почти так же, как curry функция lodash.