Функция reduce

JavaScript в последние годы набрал нешуточную популярность, в связи с чем его подводные камни также стали явственно видны. Справедливости ради, стоит отметить, что любой язык в некоторой мере имеет как своё legacy, так и подводные камни.Конкретно JavaScript обладает целым огородом камней. Подводным огородом.На практике, подводные камни встречаются не так часто, напротив, хороший код склонен быть описанным в рамках здорового подмножества языка. Это также является и причиной, почему запомнить все заковырки языка достаточно сложно: они не являются необходимыми для каждодневной практики. Тем не менее, разнообразные граничные случаи использования языковых конструкций это отличная разминка для ума, а также стимул узнать язык немного лучше. Сегодняшний экземпляр попался мне на глаза в процессе прохождения JavaScript Puzzlers.

Меня заинтересовал вопрос номер 3: Каков результат этого выражения (или нескольких)?

[ [3,2,1].reduce (Math.pow), [].reduce (Math.pow) ] В качестве ответа авторами, на выбор, даются следующие варианты:* ошибка* [9, 0]* [9, NaN]* [9, undefined]Попробуйте и вы, без запуска интерпретатора, пользуясь только своим умом ответить на этот вопрос.

Несмотря на то, что пример достаточно отстранённый, аппликация функций и частично определённых функций к коллекциям это распространённая практика для JS, и, при здравом использовании, она способна сделать код чище, как в плане исполнения — избавить его от лишних замыканий, так и в визуальном плане — меньше скобочного мусора (вопрос использования препроцессоров оставим для другой статьи).

А в этой статье вы найдёте:* Разбор задачки.* JavaScript reduce с чисто практической точки зрения.* Несколько акробатических этюдов с reduce (reduce с академической точки зрения).* Репозиторий с плюшками к статье.* Несколько других reduce.

Разбор задачки Чтож, для начала разберёмся с задачей в начале статьи. А вариантов здесь хватает.reduce (здесь и далее имеется ввиду Array.prototype.reduce), вместе с другими функциями из прототипа Array: filter, map, forEach, some, every, является функцией высшего порядка, то есть она принимает на вход другую функцию (будем называть эту передаваемую функцию f*). Функция f* будет вызвана с некоторыми агрументами для каждого элемента коллекции.Конкретно reduce, используется для генерации некоторого агрегирующего значения на основе коллекции. Она последовательно применяет f* к каждому элементу, передавая ей текущее значение переменной, в которой накапливается результат (аккумулятора) и текущий обрабатываемый элемент. Также, в reduce можно передать начальное значение аккумулятора. Причём, (!) поведение reduce будет различаться в зависимости от того, передано это значение или нет.

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

При этом остаются открытыми следующие вопросы:* Как ведёт себя reduce, если вызвать её на пустом массиве?* Как ведёт себя Math.pow, если недодать ей степень?

Для стандартных функций JS нет общей политики обработки ошибок. Некоторые функции могут действовать строго: бросать исключения, если что-то не так в переданных данных, некоторые будут возвращать всяческие пустые значения: null, undefined, NaN, а прочие будут работать пермиссивно: попытаются что-то сделать даже с не совсем корректными данными.

Как много вопросов затронул всего один пример.

А теперь правильный ответ: мы получим TypeError, в котором виновато второе подвыражение. Функция reduce на пустом массиве И без переданного начального значения бросает TypeError.

Почему так? Вчитываемся в спецификацию reduce Чтож, давайте почитаем что пишет MDN o Array.prototype.reduce. Выясняются следующие тонкости работы функции: Если initialValue передано, то на первой итерации функция будет вызвана с этим значением и значением первого элемента массива. Если же, initialValue не передано, то функция будет вызвана со значениями первого и второго элементов массива. Отсюда также следует, что если начальное значение не передано, то функция вызывается на один раз меньше, иначе ровно столько раз, сколько элементов в массиве.Можно представлять форму с initialValue вот так:

array.reduce (fn, initialValue) ⇔ [ initialValue ].concat (array).reduce (fn); Вторым интересным аспектом является обработка пустого массива. Если массив пустой, и передано начальное значение, то оно является результатом работы функции, а результат f* игнорируется. Если же массив пуст, а начальное значение не передано, то выбрасывается TypeError. [].reduce (fn, initialValue) ⇔ [ initialValue ].reduce (fn) ⇒ initialValue; [].reduce (fn) ⇒ TypeError; На самом деле поведение функции достаточно логично: она пытается вызвать f* со значениями из входных данных. Если начальное значение передано, то оно является элементом данных идущим перед первым элементом. Если не передано ничего (нет элементов и начального значения), то функция не имеет данных для генерации агрегата, и она выбрасывает исключение. Так или иначе, поведение немножко сложное и может стать подводным камнем. reduce, по сути, перегружается для одного агрумента и для двух, и перегруженные варианты имеют разное поведение на пустом массиве.Теперь можно понять, почему задачка имеет такой результат, а именно, второе подвыражение бросает исключение: оно вызывается с пустым входным списком и без стартового значения. Но! Первое подвыражение всё-таки вычислилось. Предлагаю в качестве упражнения попытаться разобраться в этом вычислении. Можно пойти двумя путями:* Джедайский: исполнить код в уме, зная о том как работают reduce и Math.pow.* Ковбойский: вбить в REPL этот код и попытаться подвести рассуждения под результат.

Также, можно ознакомиться с моим примером, который должен помочь понять задачку: StreetStrider/habrahabr-javascript-reduce: tests/puzzler.js. Он является jasmine-тестом.

Магия и шарм reduce reduce примечателен тем, что он может быть использован для того, чтобы описать все остальные функции высшего порядка объекта Array: forEach, filter, map, some, every.Это станет понятным, если избавиться от мысли, что reduce обязан аккумулировать значение того же типа, что и значения в массиве. Действительно, логичным кажется мыслить, что если мы берём массив чисел и суммируем их, то получаем также число. Если мы берём массив строк и конкатенируем их, то также получаем строку. Это естественно, но reduce также способен возвращать массивы и объекты. Причём передача будет происходить из итерации в итерацию благодаря аккумулятору. Это позволяет строить на reduce функции любой сложности.

Для примера, давайте построим map:

function map$viaReduce (array, fn) { return array.reduce (function (memo, item, index, array) { return memo.concat ([ fn (item, index, array) ]); }, []); }; Здесь через аккумулятор передаётся накапливающийся массив. Он будет того же размера, что и исходный, но со значениями, пропущенными через функцию-трасформатор fn. Также здесь не забыто, то fn принимает не только элемент, но индекс и массив последующими параметрами. Параметр функции concat обёрнут в массив, чтобы избежать «развёртки» значения, если fn вернёт массив. В качестве начального значения передан пустой массив.Этот код есть в репозитории, а ещё для него есть тесты.

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

Ещё один нетривиальный пример, который приходит на ум, это реализация функции uniq. Как известно, JavaScript страдает от отсутствия в стандартной либе многих нужных вещей. В частности, нет функции, которая устраняет дубликаты в массиве, и разработчики используют разные кастомные реализации (лично я советую использовать _.uniq из LoDash/Underscore).

function uniq$viaReduce (array) { return array.reduce (function (memo, item) { return (~ memo.indexOf (item) ? null: memo.push (item)), memo; }, []); }; Эта реализация использует немного JS-ного джедаизма, для лаконичности. В принципе, этот код не планируется когда-либо менять, поэтому это не наносит существенного ущерба. Здесь используется сайд-эффект внутри тернарного оператора, а именно, мы проталкиваем элемент в массив, если он не найден на текущем куске. Оператор тильда используется для сравнения с -1. Всё выражение завёрнуто в оператор запятую, который на каждом шаге (после всех действий) возвращает memo. Примечательно, что эта реализация также сохраняет порядок в массиве.Код и тесты есть в репозитории.

Ладно, не «немного», этот код был сильно джедайский, меня оправдывает только наличие тестов и то, что это библиотечная функция, поведение которой не будет меняться.

В качестве разминки, я рекомендую реализовать, например, функцию zipObject суть её в том, что она принимает на вход массив пар (массивов), где нулевой элемент это ключ, а первый — значение, и возвращает сконструированный Object с соответствующими ключами/значениями.

Подробнее о репозитории. Репозиторий с примерами является npm-пакетом. Его можно поставить, используя адрес на гитхабе: npm install StreetStrider/habrahabr-javascript-reduce В src/ лежат примеры функций, в tests/ — jasmine-тесты. Прогнать все тесты можно с помощью npm test.Другие reduce * В JavaScript у reduce есть злой брат-близнец правосторонний аналог: reduceRight. Он нужен, чтобы агрегировать массивы справа-налево, без необходимости в дорогостоящем reverse.* LoDash/Underscore есть _.reduce, _.reduceRight. Они обладают рядом дополнительных возможностей.* В Python есть reduce. Да. Но он официально не рекомендуется к использованию. Вместо него предлагается использовать списковые выражения и конструкции for-in. Кода получается больше, но он становится намного более читаемым. Это соответствует Дао языка.* В некоторых языках reduce/reduceRight называются foldl/foldr.В SQL есть пять стандартных агрегирующих функций: COUNT, SUM, AVG, MAX, MIN. Эти функции используются, чтобы свести результирующую выборку к одному кортежу. Аналогичные функции можно реализовать на JavaScript (тоже на reduce).

Кстати, четыре из пяти агрегирующих функций SQL (не считая COUNT) возвращают NULL, если выборка пустая (COUNT возвращает определённое значение: 0). Это полностью аналогично JS-ному TypeError на пустом списке.

postgres=# SELECT SUM (x) FROM (VALUES (1), (2), (3)) AS R (x); sum ----- 6 (1 row) postgres=# SELECT SUM (x) IS NULL AS sum FROM (VALUES (1), (2), (3)) AS R (x) WHERE FALSE; sum ----- t (1 row)

Ссылки JavaScript Puzzlers. MDN: Array.prototype.reduce (). github: StreetStrider/habrahabr-javascript-reduce. JavaScript.ru: Массив: Перебирающие методы. Благодарности Спасибо subzey за то, что натолкнул меня на мысль, что reduce может возвращать что угодно.Спасибо всем, кто напишет мне в личные сообщения об ошибках и недочётах в статье, а также в репозитории.Спасибо за внимание.

© Habrahabr.ru