Функции высшего порядка в Lodash

Перевод статьи Michał Piotrkowski: Higher-order functions in Lodash.

В этой статье я хочу объяснить концепцию функций высшего порядка и как они повсеместно представлены в моей любимой JavaScript библиотеке — Lodash.

Функции высшего порядка — это отличный способ сделать код более гибким и переиспользуемым, а также более декларативным. Перед тем, как перейти к определению, давайте взглянем на простой пример. Мы определим функцию, перемножающую два числа:

function multiply(a, b) {
return a * b;
}

Давайте немного поиграем с этой функцией:

> multiply(21, 2)
< 42

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

function double(v) {
return multiply(v, 2);
}

Теперь мы можем легко удваивать значения:

> double(5)
< 10

Перед нами пример классического делегирования функции (одна функция делегирует другой).

Однако это не единственный возможный способ достижения подобного результата. В языках, в которых функции являются объектами первого класса (таких как JavaScript), существует другой, более функциональный, способ определить функцию double(). Он известен как частичное применение функции.

Частичное применение

Согласно Википедии:

Частичное применение — процесс фиксации части аргументов функции, который создает другую функцию, меньшей арности.

С частичным применением мы можем создать функцию double() следующим образом:

var double = partial(multiply, 2);

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

function partial(fn) {
var fixed = [].slice.apply(arguments, [1]); /* 1 */
return function() { /* 2 */
var args = fixed.concat([].slice.apply(arguments));
return fn.apply(this, args); /* 3 */
};
}

Наша функция берет и сохраняет в локальной переменной (fixed) все параметры, кроме первого (1). Затем она возвращает новую функцию (2), вызывающую исходную функцию fn со списком параметров, в который в начало добавлены зафиксированные параметры, сохраненные в переменной fixed (3). Эта реализация на чистом JavaScript упрощена, тем не менее она довольно мощная.

Но подождите: обратите внимание на уродливую реализацию вызовов slice.apply(). Они необходимы, потому что объект arguments в JavaScript - это не настоящий массив, поэтому он не имеет метода slice, так что мы используем Function.prototype.apply().

Но если мы используем ECMAScript 2015 (ES6), мы можем упростить код, используя оператор rest:

function partial(fn, ...args) {
return function(...newArgs) {
return fn.apply(this, args.concat(newArgs));
};
}

Однако, если мы хотим придерживаться ES5, мы можем переписать функцию, используя возможности Lodash. Везде, где в качестве параметра ожидается массив, Lodash принимает arguments как параметр функции. Вы также можете легко конвертировать любой похожий на массив объект в настоящий массив с использованием _.toArray(). Наша улучшенная реализация будет выглядеть так:

function partial(fn) {
var fixed = _.tail(arguments);
return function() {
return fn.apply(this, _.concat(fixed, arguments));
};
}

Но к счастью, мы не обязаны писать собственную реализацию partial, так как Lodash уже имеет собственную: _.partial(). Более того, как вы можете увидеть в следующем примере, она более мощная, чем наш упрощенный пример.

Скажем, у нас есть функция divide(), которая делит одно число на другое:

function divide(a, b) {
return a / b;
}

Теперь мы хотим переиспользовать нашу функцию divide(a, b), чтобы создать новую функцию half(n), которая делит данное число пополам, подобно нашему предыдущему сценарию. На первый взгляд, она будет похожа на наш пример с multiple/double. Однако код...

var half = _.partial(divide, 2);
> half(4);
< 0.5

… возвращает, как вы видите, неверный результат.

Это происходит, потому что порядок параметров неверный (умножение является коммутативной операцией). Теперь мы хотим фиксировать второй параметр и оставить первый параметр свободным. Мы не можем сделать это с нашей простой реализацией, но, к счастью, авторы Lodash предусмотрели это и предоставили решение для подобных случаев. В Lodash мы можем пропускать фиксацию параметра, используя заместитель (placeholder) следующим образом:

var half = _.partial(divide, _, 2);
var invert = _.partial(divide, 1);

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

> half(5);
< 2.5
> invert(5);
< 0.2

Каррирование

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

> var divideC = _.curry(divide);
> divideC(4, 2)
< 2
> divideC(4)(2)
< 2
> divideC(4)
< [Function]
> var half = divideC(_, 2);
> half(4)
< 2

После преобразования функции с помощью _.curry() мы получаем совершенно новую функцию, накапливающую и фиксирующую параметры для последующих вызовов, пока все ожидаемые параметры не будут указаны - в таком случае исходная функция будет вызвана. Параметры могут быть указаны один за другим или по несколько сразу. Как вы видите, вы можете также пропускать параметры, используя заместитель _, так же как и в _.partial().

Ограничения _.curry()

_.curry() более мощная функция, чем _.partial(), но она также имеет некоторые ограничения. Взглянем на следующий пример:

> parseInt('123')
< 123
> var parseIntC = _.curry(parseInt);
> parseIntC('123')
< [Function]
> parseIntC('123')(10)
< 123

Что здесь происходит? ParseInt(string, radix=10) имеет второй, необязательный параметр. Lodash не может указать, какую арность на самом деле имеет функция, и полагает, что арность основывается на свойстве Function.prototype.length, равном числу параметров, указанному в определении функции. Похожие ситуации возникают, когда функции принимают переменное число параметров и используют объект arguments (так называемые вариативные параметры). Это может приводить к крайне неожиданным и подверженным ошибкам результатам. В таких случаях рекомендуется указать точную арность функции при каррировании:

> var parseIntC = _.curry(parseInt, 1);
> parseIntC('123')
< 123

Другие функции высшего порядка в Lodash

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

Вся библиотека Lodash полна функций высшего порядка. Самые примечательные из них: _.identity(), _.negate(), _.memoize(), _.constant(), _.property(), _.iteratee(), _.matches(), _.conforms(), _.overSome(), _.overEvery(), _.flow().

Если вы используете Lodash (или планируете использовать) ежедневно, полезно знать об их существовании, так как они значительно уменьшают количество кода. Кроме того, они улучшают его читаемость. Я не буду вдаваться в детали описания каждой из этих функций, поскольку Lodash docs отлично с этим справляются. Я только сделаю одно исключение: _.flow(). Функция flow - одна из самых используемых во всей библиотеке. Она позволяет составлять новые функции из цепочки других функций, указывая их одну за другой. Результат (возвращаемое значение) каждой функции в этой последовательности становится входным параметром для следующей функции. Это похоже на оператор pipe (|) в Linux bash. В математике это классическая композиция функции:

_.flow([f, g, h])(x) <=> f(g(h(x)))

Спасибо _.flow(), что теперь так легко собирать новые функции из существующих:

var sumAll = _.flow([_.concat, _.flattenDeep, _.sum]);
_.sum(1, 2, [3, 4]);
> 0
sumAll(1, 2, [3, 4]);
> 10

_.flow() похож на _.chain(), однако, в отличие от _.chain(), фиксирующей данные при первом вызове, результатом _.flow() является функция, принимающая данные в конце. Это означает, что она может быть присвоена переменной или передана как параметр, позволяя эффективно переиспользовать ее для различных наборов данных.

Ниже вы можете увидеть код, который подсчитывает 5 стран с крупнейшими городами в мире. Я использовал нотацию стрелочных функций из ES2015 для краткости:

var cities = require('./cities.json');
_(cities)
.filter(c => c.population >= 5000000)
.countBy(c => c.country)
.toPairs()
.map(c => _.zipObject(['country', 'numOfCities'], c))
.orderBy(c => c.numOfCities, 'desc')
.take(5)
.value();

cities.json содержит данные про 91 крупнейший город в мире. Данные про население были взяты из Википедии.

Теперь давайте используем _.partial() и _.curry(), чтобы переписать этот пример:

var greatherThan = threshold => _.partial(_.gte, _, threshold);
var populationGreatherThan = threshold => _.conforms({ population: greatherThan(threshold) });
var zipObject = _.curry(_.zipObject);
_(cities)
.filter(populationGreatherThan(5000000))
.countBy(_.property('country'))
.toPairs()
.map(zipObject(['country', 'numOfCities']))
.orderBy(_.property('numOfCities'), 'desc')
.take(5)
.value();

Также мы можем определить var greatherThan = _.curryRight(_.gte). _.curryRight() похожа на _.curry(), но она фиксирует параметры в обратном порядке (начиная с последнего).

Более того, для функций, принимающих перебирающий (iteratee) аргумент (как _.map(), _.countBy(), _.groupBy()), Lodash автоматически оборачивает перебирающий аргумент с помощью функции _.iteratee(), которая, в конечном счете, для параметров типа string делегирует функции _.property(). Так что наш код может быть упрощен и далее:

var greatherThan = _.curryRight(_.gte)
var populationGreatherThan = threshold => _.conforms({ population: greatherThan(threshold) });
var zipObject = _.curry(_.zipObject);
_(cities)
.filter(populationGreatherThan(5000000))
.countBy('country')
.toPairs()
.map(zipObject(['country', 'numOfCities']))
.orderBy('numOfCities', 'desc')
.take(5)
.value();

Lodash/fp

Когда я научился обращаться с _.curry() и _.partial(), я заметил, что почти все время я каррирую большую часть функций. Кроме того, я стараюсь пропускать первый параметр (или использовать вариант *Right()вышеупомянутых функций).

Затем я наткнулся на вариацию lodash/fp библиотеки Lodash, предлагающей более функциональный стиль, экспортируя объект lodash с методами, обернутыми таким образом, чтобы создавать неизменяемые авто-каррируемые перебирающие-в-начале (iteratee-first) методы c данными в конце.

Lodash/fp, в основном, предлагает следующие изменения:

  1. каррируемые функции: все функции являются каррируемыми по умолчанию;
  2. фиксированная арность: все функции имеют фиксированную арность, что решает показанную ранее проблему с каррированием. Любые функции, имеющие необязательные параметры, делятся на две отдельные функции (например, _.curry(fn, arity?) делится на _.curry(fn) и _.curryN(fn, arity));
  3. переставленные параметры: параметры функций переставлены, так что данные принимаются в качестве последнего параметра, потому что в реальной жизни наибольшее число времени вы хотите зафиксировать перебирающие параметры и оставить параметры для данных свободными;
  4. неизменяемые параметры: функции не изменяют переданные параметры, но возвращают измененные копии объектов;
  5. ограниченные перебирающие функции обратного вызова: имеют арность сведенную к 1, так что они избегают проблем с каррированием (пункт 2);
  6. больше никаких цепочек: формирование цепочек функций с помощью _.chain() или _() больше не поддерживается (вместо этого можно использовать _.flow()).

Для более детального описания каждого изменения обратитесь к Lodash FP guide. Вкратце, все эти изменения выливаются в гораздо более декларативный, меньше подверженный ошибкам, свободный от шаблонов код.

В стиле lodash/fp в очередной раз переписанный пример будет выглядеть так:

_.flow([
_.filter(_.conforms({ population: _.gte(_, 5000000) })),
_.countBy('country'),
_.toPairs,
_.map(_.zipObject(['country', 'numOfCities'])),
_.orderBy('numOfCities', 'desc'),
_.take(5)
])(cities);

Как вы можете видеть, здесь больше нет вызовов _.curry(), поскольку функции каррированы по умолчанию. Вызов _.chain() был заменен на _.flow(), а параметр cities передан в конце. Если мы сохраним результат _.flow() в переменную, то позже мы сможем переиспользовать ее для различных данных из cities.

Резюме

Понимание функций высшего порядка, особенно _.partial() и _.curry(), является ключевым, если мы хотим получить наибольшую пользу от функциональных библиотек, таких как Lodash.

В моей следующей статье я покажу, что Lodash — это библиотека не только для манипулирования списками.

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


Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.

Статья на GitHub