Привет, Хабр! Представляю вашему вниманию перевод статьи "Five Interesting Ways to Use Array.reduce() (And One Boring Way)" автора Chris Ferdinandi.
Из всех современных методов работы с массивами самым сложным из всех, что мне пришлось использовать, был Array.reduce().
На первый взгляд он кажется простым, скучным методом, который мало что дает. Но, не смотря на свой скромный вид, Array.reduce() является мощным и гибким дополнением к вашему набору инструментов разработчика.
Сегодня рассмотрим некоторые интересные вещи, которые можно сделать с помощью Array.reduce().
Как работает Array.reduce()
Большинство современных методов массива возвращают новый массив. Метод Array.reduce() немного более гибкий. Он может вернуть все что угодно. Его цель — взять массив и сжать его содержимое в одно значение.
Это значение может быть числом, строкой или даже объектом или новым массивом. Это та часть, которая всегда сбивала меня с толку — я не понимал, насколько она гибкая!
Синтаксис
Array.reduce() принимает два аргумента: метод callback, выполняемый для запуска каждого элемента в массиве, и начальное значение initialValue.
Callback также принимает два аргумента: accumulator, который является текущим объединенным значением, и текущий элемент в цикле currentValue. Все, что вы возвращаете, используется в качестве accumulator для следующего элемента в цикле. В самом первом цикле вместо этого используется начальное значение.
var myNewArray = [].reduce(function (accumulator, current) {
return accumulator;}, starting);
}, starting);
Рассмотрим несколько примеров
var myNewArray = [].reduce(function (accumulator, current) {
return accumulator;}, starting);
1.Суммирование чисел
Допустим, у есть массив чисел, которые хотим сложить вместе. Используя Array.forEach(), можем сделать что-то вроде этого:
var total = 0;
[1, 2, 3].forEach(function (num) {
total += num;
});
Это пример-клише для использования Array.reduce(). Слово «accumulator» сбивает с толку, поэтому в этом примере назовем его «sum», потому что это то, что оно есть по своей сути.
var total = [1, 2, 3].reduce(function (sum, current) {
return sum + current;
}, 0);
Здесь мы передаем 0 как наше начальное значение.
В обратном вызове мы добавляем текущее значение к сумме, которая имеет начальное значение 0 в первом цикле, затем 1 (начальное значение 0 плюс значение элемента 1), затем 3 (суммарное значение 1 плюс значение элемента 2) и так далее.
Пример.
2.Альтернатива комбинированию методов массива Array.map() и Array.filter() в одном шаге
Представим, что в Хогвартсе множество волшебников.
var wizards = [
{
name: 'Harry Potter',
house: 'Gryfindor'
},
{
name: 'Cedric Diggory',
house: 'Hufflepuff'
},
{
name: 'Tonks',
house: 'Hufflepuff'
},
{
name: 'Ronald Weasley',
house: 'Gryfindor'
},
{
name: 'Hermione Granger',
house: 'Gryfindor'
}];
Хотим создать новый массив, который будет содержать только имена мастеров из Хаффлпаффа. Один из способов сделать это — использовать метод Array.filter(), чтобы получить обратно только тех волшебников, у которых свойство дома — Хаффлпафф. Затем используем метод Array.map() для создания нового массива, содержащего только свойство name для остальных мастеров.
// Получаем имена волшебников из Хаффлпафф
var hufflepuff = wizards.filter(function (wizard) {
return wizard.house === 'Hufflepuff';
}).map(function (wizard) {
return wizard.name;
});
С помощью метода Array.reduce() можно получить один и тот же массив за один проход, что улучшит нашу производительность. Передаем пустой массив ([]) в качестве начального значения. На каждом проходе проверяем, является ли wizard.house Хаффлпаффом. Если это так, отправляем его в newArr (наш accumulator в этом примере). Если нет, ничего не делаем.
В любом случае, возвращаем newArr, чтобы получить accumulator на следующем проходе.
// Получаем имена волшебников из Хаффлпафф
var hufflepuff = wizards.reduce(function (newArr, wizard) {
if (wizard.house === 'Hufflepuff') {
newArr.push(wizard.name);
}
return newArr;
}, []);
3.Создание разметки из массива
Что если вместо создания массива имен, хотим создать неупорядоченный список мастеров в Хаффлпаффе? Вместо пустого массив в Array.reduce() в качестве нашего начального значения, передадим пустую строку ('') и назовем ее html.
Если wizard.house равен Hufflepuff, мы объединяем нашу html-строку с wizard.name, обернутым в открывающий и закрывающий элементы списка (li). Затем вернем HTML, как accumulator в следующем цикле.
// Создание списка волшебников из Хаффлпафф
var hufflepuffList = wizards.reduce(function (html, wizard) {
if (wizard.house === 'Hufflepuff') {
html += '<li>' + wizard.name + '</li>';
}
return html;
}, '');
Добавим открывающий и закрывающий неупорядоченный элемент списка до и после Array.reduce(). Теперь все готово для добавления разметки в DOM.
// Создание списка волшебников из Хаффлпафф
var hufflepuffList = '<ul>' + wizards.reduce(function (html, wizard) {
if (wizard.house === 'Hufflepuff') {
html += '<li>' + wizard.name + '</li>';
}
return html;
}, '') + '</ul>';
4.Группировка похожих элементов в массив
В библиотеке lodash есть метод groupBy(), который принимает коллекцию элементов в виде массива и группирует их в объект на основе некоторых критериев.
Допустим, нам нужен массив чисел.
Если хотим сгруппировать все элементы в числа по их целочисленному значению, то сделать это следует с помощью lodash.
var numbers = [6.1, 4.2, 6.3];
// returns {'4': [4.2], '6': [6.1, 6.3]}
_.groupBy(numbers, Math.floor);
Если имеется массив слов, и нужно сгруппировать элементы в словах по их длине, мы бы это сделали.
var words = ['one', 'two', 'three'];
// returns {'3': ['one', 'two'], '5': ['three']}
_.groupBy(words, 'length');
Создание функции groupBy() с помощью Array.reduce()
Можно воссоздать ту же функциональность, используя метод Array.reduce().
Cоздадим вспомогательную функцию groupBy(), которая принимает массив и критерии для сортировки в качестве аргументов. Внутри groupBy() мы будем запускать Array.reduce() для нашего массива, передавая пустой объект ({}) в качестве отправной точки и возвращая результат.
var groupBy = function (arr, criteria) {
return arr.reduce(function (obj, item) {
// Some code will go here...
}, {});
};
Внутри Array.reduce() функцией callback проверим, является ли критерий функцией, применяемой к элементу, или же свойством элемента. Тогда мы получим его значение из текущего элемента.
Если в объекте пока нет свойства с этим значением, создадим его[свойство] и назначим пустой массив в качестве его значения. Наконец, добавим элемент в это свойство и вернем объект в качестве accumulator для следующего цикла.
var groupBy = function (arr, criteria) {
return arr.reduce(function (obj, item) {
// Проверка на то, является ли критерий функцией элемента или же //свойством элемента
var key = typeof criteria === 'function' ? criteria(item) : item[criteria];
// Если свойство не создано, создаем его.
if (!obj.hasOwnProperty(key)) {
obj[key] = [];
}
// Добавление значения в объект
obj[key].push(item);
// Возвращение объекта для следующего шага
return obj;
}, {});};
Демонстрация завершенной вспомогательной функции.
Отдельное спасибо Тому Бремеру за помощь. Эту вспомогательную функцию и многое другое можно найти в Vanilla JS Toolkit.
5.Объединение данных из двух источников в массив
Вспомним наш список волшебников.
var wizards = [
{
name: 'Harry Potter',
house: 'Gryfindor'
},
{
name: 'Cedric Diggory',
house: 'Hufflepuff'
},
{
name: 'Tonks',
house: 'Hufflepuff'
},
{
name: 'Ronald Weasley',
house: 'Gryfindor'
},
{
name: 'Hermione Granger',
house: 'Gryfindor'
}];
Что делать, если бы был другой набор данных — объект c домом и очками, которые заработал каждый маг.
var points = {
HarryPotter: 500,
CedricDiggory: 750,
RonaldWeasley: 100,
HermioneGranger: 1270
};
Представим, что хотим объединить оба набора данных в один массив с количеством очков, добавленных к данным каждого волшебника в массиве wizards. Как это сделать?
Метод Array.reduce() идеально подходит для этого!
var wizardsWithPoints = wizards.reduce(function (arr, wizard) {
// Получаем значение для объекта points, удалив пробелы из имени //волшебника
var key = wizard.name.replace(' ', ' ');
// Если у волшебника есть очки, устанавливаем значение,
// иначе устанавливаем 0.
if (points[key]) {
wizard.points = points[key];
} else {
wizard.points = 0;
}
// Добавляем объект wizard в новый массив.
arr.push(wizard);
// Возвращаем массив.
return arr;
}, []);
Пример объединения данных из двух источников в массив.
6.Объединение данных из двух источников в объект
Что, если вместо этого необходимо объединить два источника данных в объект, в котором имя каждого волшебника это ключ (key), а их дом и очки — свойства? Опять же, метод Array.reduce() идеально подходит для этого.
var wizardsAsAnObject = wizards.reduce(function (obj, wizard) {
// Получаем значение ключа для объекта points, удалив пробелы из имени
//волшебника
var key = wizard.name.replace(' ', ' ');
// Если у волшебника есть очки, устанавливаем значение,
// иначе устанавливаем 0.
if (points[key]) {
wizard.points = points[key];
} else {
wizard.points = 0;
}
// Удаляем свойство name
delete wizard.name;
// Добавляем значение wizard в новый объект
obj[key] = wizard;
// Возвращаем массив
return obj;
}, {});
Пример объединения данных из двух источников в объект.
Стоит ли использовать Array.reduce()?
Метод Array.reduce() превратился из бессмысленного в мой любимый метод JavaScript. Итак, стоит ли его использовать? И когда же?
Метод Array.reduce() обладает фантастической поддержкой браузеров. Работает как во всех современных браузерах так и в IE9. Уже долгое время поддерживается мобильными браузерами. Если нужно еще больше, то можно добавить полифилл, чтобы вернуть поддержку в IE6.
Самой серьезной проблемой может быть то, что Array.reduce() сбивает с толку людей, которые никогда не сталкивались с ним[методом] раньше. Комбинация методов Array.filter() с Array.map() выполняется медленнее и включает дополнительные шаги, но ее легче читать. Из названий методов видно, что они должны делать.
Как уже было сказано, метод Array.reduce(), в целом, упрощает более сложные вещи. Хорошим примером является вспомогательная функция groupBy().
В конечном счете, это еще один инструмент для вашего инструментария. Инструмент, который, если его правильно использовать, может дать сверхспособности.
Об авторе
Крис Фердинанди помогает людям изучать ванильный JavaScript. Он считает, что есть более простой и надежный способ делать вещи для интернета.
Крис является автором серии Vanilla JS Pocket Guide, создателем учебной программы Vanilla JS Academy и ведущим Vanilla JS Podcast. Его бюллетень советов разработчикам читают тысячи разработчиков каждый будний день.
Он обучал разработчиков в таких организациях, как Chobani и Boston Globe, а его плагины JavaScript были использованы Apple и Гарвардской школой бизнеса. Крис Койер, основатель CSS-Tricks и CodePen, описал его работу как "бесконечно цитируемую".
Крис любит пиратов, щенков и фильмы Pixar, а также живет рядом с лошадиными фермами в сельской местности Массачусетса. Он ведет Go Make Things с щенком Бейли.
Комментарии (66)
TheShock
08.01.2020 16:12В большинстве примеров совершенно непонятно зачем вообще использовать reduce, когда значительно лучше подходит обычный for.
Для себя решил, что единственное, где стоит использовать reduce — это если можешь написать абстрактную чистую функцию под него, которую передать в качестве аргумента. К примеру:
const add = (a, b) => a + b; // вот тут редьюс адекватно: const sum = numbers => numbers.reduce(add, 0)
Во всех остальных случаях — он слишком нечитабельныйsshikov
08.01.2020 17:08for — это побочный эффект. Теоретически, внутри цикла вы можете делать все что угодно — и синтаксически это вообще не видно. Так что вопрос читабельности — он спорный. Хотя да, reduce штука достаточно абстрактная, и потому в смысле читабельности тоже далеко не всегда простая. map и filter в этом смысле сильно проще.
>если можешь написать абстрактную чистую функцию
А что вам собственно может помешать?TheShock
08.01.2020 17:16А что вам собственно может помешать?
Потому что у ваших примеров функции «грязные» — не puresshikov
08.01.2020 17:23Ну, во-первых — это не мои примеры. А во-вторых, для reduce функция свертки как раз обычно должна быть чистой. У нее два аргумента, один из которых аккумулятор — вот с ним она может и должна делать что угодно. И это не будет грязной функцией. Если же ей нужно менять что-то другое — я бы сказал, что это явный признак применения reduce не по назначению.
TheShock
08.01.2020 17:30Вот, видите. Вы не пишете абстрактную чистую функцию. Вы или пишете «абстрактную грязную», или «вроде бы как чистую, но только если она применяется в reduce»
И это не будет грязной функцией
Будет, по определению чистой функции. В данном случае — изменение переданного аргумента — побочный эффектsshikov
08.01.2020 17:55Насколько я знаю, reduce не использует измененный аккумулятор после функции свертки — она использует возвращаемое новое значение аккумулятора. Во всяком случае это было бы логично. Так что написать грязную функцию вы конечно можете, но вы не обязаны делать ее грязной.
TheShock
08.01.2020 18:14Зависит от того, как использовать. Сама редьюс не меняет, но почти все примеры в статье — грязные.
Druu
09.01.2020 00:00Чистота — понятие относительное. Если вы, например используете arr.push(x) внутри reduce, но при этом на arr только одна единственная ссылка — то вы можете смело считать arr.push(x) обычным сахаром над arr = [ ...arr, x ]. В итоге ф-я является чистой, т.к. мутаций никаких не происходит — вы, с точки зрения функциональной семантики, создаете каждый раз новый массив, а не мутируете старый.
При этом уникальность, конечно, контролировать вам руками придется.
С точки зрения практики для таких кейзов (с мутацией уникального объекта) можно сделать ф-ю обертку, чтобы явно нотировать этот момент для стороннего читателя, т.е. вместо arr.push(x) писать uniq_mutate(arr, (a) => a.push(x)), uniq_mutate x f = f x
TheShock
09.01.2020 02:47Да, я понимаю, что внешняя функция получается чистой. Но почитайте мой изначальный посыл. По моему мнению редьюс стоит использовать тогда, когда можно написать абстрактную чистую функцию под него. Это просто способ понять, подходит ли здесь редьюс. В остальных случаях больше подходит, к примеру, обычный цикл
Druu
09.01.2020 08:21Да, я понимаю, что внешняя функция получается чистой.
Так и внутренняя чистая, раз сайд-эффект пронаблюдать вы не можете.
Лучше чем forEach оно тут тем, что init не утекает из контекста. Т.е., цикл нужен вам как раз тогда, когда внутренняя функция не может быть сведена к чистой, т.к. вы должны изменить init. Если же у вас одна ссылка на init то вам его менять не надо, тогда внутренняя ф-я чистая и редьюс сам тоже чистый, несмотря на то, что в нем всякие arr.push и прочие вещи.
Прямо ваш собственный пример выше это хорошо демонстрирует — у вас hufflepuffList находится во внешнем по отношению к for контексте, а вот html в варианте с редьюсом никуда за пределы фунарга редьюса не уходит. Вот когда вам надо, чтобы переменная существовала за пределами контекста цикла (а тут этого не надо), то, конечно, предпочтительнее for/forEach. В противном случае лучше использовать редьюс — именно за тем, чтобы показать, что "этот кусок кода не срет наружу". Ну а с циклом — с-но, что наружу срет.
TheShock
09.01.2020 12:36Так и внутренняя чистая, раз сайд-эффект пронаблюдать вы не можете.
Так могу же. Если у меня функция написана отдельно — я могу пронаблюдать сайд-эффект в ней. Я все функции стараюсь писать абстрактно с адекватным именованием. Или хотя бы так, чтобы их к такому состоянию можно было легко отрефакторить. На пример с фильтром и мапом этот подход прекрасно ложится. Редьюс — пованивает. Это не для того, чтобы сделать функцию принципиально чистой. Для меня это признак удачно написанного кода или неудачно. Лично для себя.
function isHufflepuff (wizard) { return wizard.house === 'Hufflepuff'; } function getName (item) { return item.name; } var hufflepuff = wizards .filter(isHufflepuff) .map(getName);
Да, я понимаю, что если закрыть глаза, то не видно, что в редьюс функция грязная (в обоих значениях) — с точки зрения архитектуры она использует сверхзнание, о том, где и как она будет вызываться и только в этих конкретных условиях работает корректно. Да, я понимаю, что комплексно с функцией-вызывателем оно вроде бы как чисто. Но я старають поддерживать модульный подход, разбивать на небольшие абстракции. И стараюсь, чтобы эти абстракции не смотрели вверх.
В противном случае лучше использовать редьюс — именно за тем, чтобы показать, что «этот кусок кода не срет наружу». Ну а с циклом — с-но, что наружу срет.
Для этого вообще-то существует специальный инструмент — вынос кода в отдельную функцию. Так:
function renderHufflepuff (wizards) { let hufflepuffList = '<ul>'; for (const wizard of wizards) { if (wizard.house === 'Hufflepuff') { hufflepuffList += '<li>' + wizard.name + '</li>'; } } return hufflepuffList + '</ul>' }
Зачем использовать для этого несемантичный редьюс? Хотя, конечно, в этом случае значительно лучше было бы написать так:
function renderWizard (wizard) { return '<li>' + wizard.name + '</li>'; } function renderHufflepuff (wizards) { return '<ul>' + wizards.filter(isHufflepuff).map(renderWizard) + '</ul>' }
Или даже так:
function renderWizard (wizard) { return '<li>' + wizard.name + '</li>'; } function renderWizardsList (wizards) { return '<ul>' + wizards.map(renderWizard) + '</ul>' } function renderHufflepuffOnly (wizards) { return renderWizardsList(wizards.filter(isHufflepuff)); } // внезапно код стало легко реюзать: function renderSlytherinOnly (wizards) { return renderWizardsList(wizards.filter(isSlytherin)); }
Я вообще не очень представляю, где может быть нужен редьюс. Он всегда лишний и выглядит словно программист просто пишет в стиле: «смотрите как я могу!»
hrie
09.01.2020 14:53Зачем использовать для этого несемантичный редьюс? Хотя, конечно, в этом случае значительно лучше было бы написать так:
Я никак не могу понять, какое значение вы вкладываете в слово «семантичный»?TheShock
09.01.2020 15:46У редьюсера есть задача, о которой вы сами говорили. Использование его для сторонних задач — нарушает его семантику. К примеру, использовать filter, чтобы вызвать метод, но не использовать его значение для фильтрации — нарушение семантики. Такой код — воняет, хотя прекрасно справляется с задачей:
function loadAll (images) { images.filter(function (img) { img.load(); }); }
Тут не подойдёт ни map, ни filter, ни reduce с точки зрения семантики.
Только for или forEach.hrie
09.01.2020 16:40Понял вас. По большей части согласен. Кроме одного момента.
Главная задача reduce, как свёртки — «… преобразование структуры данных к единственному атомарному значению...» (Википедия). То есть для реализации функции renderHufflepuff семантически он подходит идеально — берётся массив, обрабатывается, из него получается строка.
А вот эта реализация как-раз «несемантична» и некорректна:
function renderHufflepuff (wizards) { return '<ul>' + wizards.filter(isHufflepuff).map(renderWizard) + '</ul>' }
Некорректна, потому что пропущен .join('') в конце; несемантична, потому что задача map (в данном случае как функтора массивов) — преобразовать из одного массива в другой с сохранением количества элементов; задача filter — удалить элементы из массива. Но, результатом их работы всегда должен быть массив! А вот если задача из массива получить строку — семантично использовать reduce ).
С другой стороны в JS Array#join — это частный случай свёртки. В конечном итоге получается, что оба варианта реализации renderHufflepuff через wizards.filter(isHufflepuff).map(renderWizard).join('') и reduce одинаково семантичны.TheShock
09.01.2020 17:00Некорректна, потому что пропущен .join('') в конце
Тут вы правы, забыл.
несемантична, потому что задача map (в данном случае как функтора массивов) — преобразовать из одного массива в другой с сохранением количества элементов
Тут — неправы, ведь именно эта задача и решается. Я из массива сырых объектов получаю массив отрендеренных строк. Что там вызывающий код с этим делать будет — не собачье дело мапа)
и reduce одинаково семантичны.
Нет, ибо редьюс кроме своей прямой обязанности объединения — ещё берёт на себя обязаность по фильтрации и маппингу.hrie
09.01.2020 17:27Нет, ибо редьюс кроме своей прямой обязанности объединения — ещё берёт на себя обязаность по фильтрации и маппингу.
Не согласен. Прямая обязанность редьюса — «… преобразование структуры данных к единственному атомарному значению...». Что будет сделано в процессе и каково будет атомарное значение — неважно. Например, поиск минимального и максимального значения массива — это тоже свёртка.
Пруфы: в Functional Programming with Bananas, Lenses, Envelopes and Barbed Wire явным образом в числе примеров катаморфизма (с т. з. теории категорий функция свёртки является катаморфизмом) перечислены и нахождение длины массива и фильтрация.
Druu
09.01.2020 21:09Так могу же.
Нет, в том и дело :)
Если у меня функция написана отдельно — я могу пронаблюдать сайд-эффект в ней
Каким образом?
Да, я понимаю, что комплексно с функцией-вызывателем оно вроде бы как чисто.
Оно вообще в принципе чисто. Без каких-либо условностей. Просто обычная чистая функция, как f x y = x + y, не знаю. Никакой разницы.
Для этого вообще-то существует специальный инструмент — вынос кода в отдельную функцию.
Так нет никакой разницы, лямбда у вас или отдельная ф-я. И та и другая может как срать так и не срать совершенно одинаково. Так что по "лямбда/отдельная функция" тут отделить нельзя никак.
Зачем использовать для этого несемантичный редьюс?
Почему не семантичный? Семантичный — у вас фунаргом идет чистая ф-я, и чтобы это показать, вы используете редьюс вместо цикла. А если бы ф-я была грязная — использовали бы цикл.
TheShock
09.01.2020 21:48Каким образом?
Таким:
function druuPureCallback(newArr, wizard) { if (wizard.house === 'Hufflepuff') { newArr.push(wizard.name); } return newArr; } function Main () { const EmptyArray = []; let result = wizards.reduce(druuPureCallback, EmptyArray); assert(0, EmptyArray.length); // failed }
Почему я передаю «чистую фунцию без сайд-эффектов по определению Druu» в редьюс, а он мне, внезапно меняет аргумент?Druu
09.01.2020 22:12Почему я передаю «чистую фунцию без сайд-эффектов по определению Druu» в редьюс, а он мне, внезапно меняет аргумент?
Так у вас ссылка неуникальная, с-но применение reduce запрещено и код не пишется. Надо: wizards.reduce(druuPureCallback, []) ну или EmptyArray = wizards.reduce(druuPureCallback, EmptyArray). Тогда ссылки на старый объект нет и применение корректно.
Во всех примерах из статьи это свойство выполнялось. Ну а если вам надо написать именно в том виде в котором вы написали — получается что колбек нечистый, т.к. срет во внешний контекст. И, раз код с reduce писать запрещено (=> такого кода не существует) используем for.TheShock
09.01.2020 22:19с-но применение reduce запрещено и код не пишется
Кем запрещено?
Так у вас ссылка неуникальная
Ну и что? В иммутабельном мире ссылка и не должна быть уникальной.
если [...] — получается что колбек нечистый, т.к. срет во внешний контекст
Без всяких «если». Колбек — нечистый.
hrie
08.01.2020 17:58+1Вопрос вкуса и привычки. Reduce, как и любая другая фича, быстро становится понятным — надо просто начать им пользоваться.
Плюс, он идеально подходит для Promise. Там функции, тут функции. В таком окружении циклы выглядят чуждыми элементами.
Ну и, спасибо разработчикам, гигантская разница в скорости между for и reduce всё более и более уменьшается.TheShock
08.01.2020 18:16Использование reduce там, где место циклу — нарушение семантики)
Чем он идеально подходит для промисов, если именно цикл корректно работает с await?
for (const img of imgs) { await asyncLoad(img); }
Попробуйте такое редьюсами написать.Zenitchik
08.01.2020 19:26imgs.reduce( (akk, img)=>akk.then(()=>asyncLoad(img)), Promise.resolve() );
В общем-то тривиально. Только за скобочками трудно следить.
BerkutEagle
09.01.2020 07:34Это нестандартное использование reduce. На счёт красоты тоже можно спорить. В поддержке же вообще — мрак. Если Вам самому «за скобочками трудно следить», каково другим будет?
Все эти «финты» — не более чем показуха.Zenitchik
09.01.2020 12:08Простите, почему "нестандартное"?
reduce предназначен для итеративного сведения массива к единственному значению. И именно это делает мой код. Возвращаемое значение — экземпляр Promise.BerkutEagle
09.01.2020 13:49Но это не сведение к одному значению — промис то пустой. И это не «выжимка» из массива, а фиктивное значение, нужное лишь для управления последовательностью задач в очереди.
Я не говорю, что делать так нельзя. Можно, но могут возникнуть проблемы. Не каждый сразу поймёт, что там происходит — не очевидно это. Умные люди говорят, что мы чаще код читаем, а читать Ваш код не всем будет легко, желательны комментарии.
Сорвётесь Вы на другой проект, а этот отдадут на сопровождение джунам — они ведь мозг могут сломать :)hrie
09.01.2020 14:48Сорвётесь Вы на другой проект, а этот отдадут на сопровождение джунам — они ведь мозг могут сломать :)
А вот это уже вопрос привычки и воспитания. Почему декларативные редьюсеры в JS с таким скрипом воспринимаются опытными программистами? Потому что в большом количестве случаев у них классический императивный бекграунд — они начинали на PHP/Python/Ruby или изучали C/Java в институте. Для них естественно мыслить циклами. Я и сам был такой.
Но. Если какое-то время после циклов писать на Promise/reduce/filter/map, то привычки начинают меняться и декларативное программирование становится куда более понятным, естественным. Начинаешь ценить его за лаконичность и идиоматичность. Приведённый пример, кстати, потому так вымученно выглядит на редьюсерах, что он из другого мира — мира синхронных языков типа PHP. JS — асинхронный язык, где всё исполняется в параллели, и асинхронное решение приведённой задачи (Promise.all(imgs.map(asyncLoad));) смотрится кратко и понятно.
Так что, если речь идёт о настоящих джунах, без опыта разработки, то им по большому счёту всё равно — изучать циклы или редьюсеры, для понимания и того и другого нужно приложить силы.Zenitchik
09.01.2020 15:29изучать циклы или редьюсеры
Чтобы понимать что ты делаешь, нужно знать, что под капотом у синтаксического сахара.
TheShock
09.01.2020 15:43Почему декларативные редьюсеры в JS с таким скрипом воспринимаются опытными программистами?
Та потому что они не декларативные. В вашем примере с промисами никакой декларативности. Вы костылём, зная как устроен промис написали магию, чтобы оно как-то заработало. Что в этом декларативного?
Если какое-то время после циклов писать на Promise/reduce/filter/map
Я лет восемь назад тоже этим увлекался. Кое-какие практики действительно годные. А кое-какие — реально идут от внутреннего: «а я ещё вот так вот могу!»hrie
09.01.2020 16:09Костылём?! Что костыльного или магичного в Promise.all(imgs.map(asyncLoad));? Чистый и понятный код.
Что значит «никакой декларативности»? Я говорю движку — пройдись по элементам массива imgs и к каждому примени функцию asyncLoad, результаты собери в массив и прокинь его в then; как ты это сделаешь — мне не важно. Это абсолютно декларативно.TheShock
09.01.2020 16:17Что костыльного или магичного в Promise.all(imgs.map
Подмена тезиса. Сначала вы говорите о «декларативных редьюсерах», а в примере приводите map, а не reduce. Фу таким быть.hrie
09.01.2020 16:50Прошу прощения, если ввёл в заблужение. Для меня map/reduce как функтор/свёртка неотделимы друг от друга. То есть и map и reduce одинаково могут быть декларативны, если они декларативно реализованы в коде.
И тем не менее, повторюсь, Что костыльного или магичного в Promise.all(imgs.map(asyncLoad));?
Zenitchik
09.01.2020 15:27+1Но это не сведение к одному значению — промис то пустой.
Погодите.
Редьюсер отвечает за сведение многих порождаемых промисов — к одному промису.
То, что промис пустой или не пустой — это уже ответственность функции, порождающей промис, а не редьюсера. И к редьюсеру это не относится никаким боком.
Ну, понимаете,
если бы я считал сумму, я бы написал
(akk, a)=>(akk+a)
если бы в массиве лежали асинхронные функции, которые нужно вызывать без аргументов, я бы написал
(akk, fun)=>(arr.then(fun))
В массиве лежат аргументы для одной и той же функции, я мог бы написать
imgs.map(img=>()=>asyncLoad(img)).reduce( (akk, fun)=>(arr.then(fun)), Promise.resolve() )
но я решил заинлайнить создание функции ()=>asyncLoad(img).
При всём уважении к программистам на других языках, для JavaScripter-a здесь не должно быть трудностей.
BerkutEagle
09.01.2020 16:02И всё равно я остаюсь при своём мнении :)
«Выжать» скаляр из массива -> reduce
Выполнить последовательно функции -> циклZenitchik
09.01.2020 16:37Выполнить последовательно функции -> цикл
Сложение, например, как в первом примере.
И я решительно не понимаю противопоставления: у reduce под капотом — тот же самый цикл.BerkutEagle
09.01.2020 16:51Отличие reduce от цикла — это наличие аккумулятора, который возвращается после прохода по массиву. И использовать reduce надо тогда, когда нам нужно и важно значение аккумулятора. (Выше ветка комментариев про семантику)
Zenitchik
09.01.2020 17:14наличие аккумулятора
Тогда я вообще не понимаю, в чём претензия к моему примеру. Редьюсер, который нанизывает промисы в цепочку друг за другом, и возвращает ссылку на последний промис — самый что ни на есть каноничный :)
С тем же успехом это мог бы быть не промис, а любой объект, у которого есть метод, принимающий аналогичный объект, и возвращающий новый аналогичный объект.
Не знаю, ну, скажем:
transforms.reduce( (akk, transform)=>(akk.matrixMul(new Matrix(transform)), new Matrix(1) )
Где Matrix — матрица, метод — матричное умножение, а transform — какие-то условные данные, по которым можно создать матрицу,
вопросов не вызывает?TheShock
09.01.2020 17:18Мои претензии:
— Он слабочитабельный в сравнении с альтернативами
— Он не декларативный
— Использование Promise.resolve — хакZenitchik
09.01.2020 17:26Мы здесь спорим не о том, что этот код плох, а о том,
сколько ангелов поместится на острие иглы, если их сажать на неё последовательноканоничное это использование reduce, или нет.
По вашим претензиям
Он слабочитабельный в сравнении с альтернативами
Согласен с поправкой, что ХУЖЕ читаем, но "слабо-" — это Вы явно утрируете.
Он не декларативный
Не вижу в этом проблемы. Цикл — вообще из императивного мира.
Promise.resolve — хак
Не более, чем Array.from. Всего лишь метод, создающий объект.
TheShock
09.01.2020 17:27каноничное это использование reduce
Про каноничность лично у меня претензии к другому примеру.
Во вашим комментариям — этот пример всем хуже цикла. Так зачем его использовать?Zenitchik
09.01.2020 17:32Так зачем его использовать?
Бог с Вами! С тех пор, как появились async/await — не надо его использовать.
hrie
09.01.2020 00:46А зачем такое писать редьюсерами, если есть Promise.all ))
Promise.all(imgs.map(asyncLoad));
Cerberuser
09.01.2020 04:09+1Семантика разная — параллельная работа в вашем случае, последовательная в ветке выше. Иногда запускать всю пачку промисов параллельно (дохренадцать запросов к API, к примеру) — всё-таки не лучший вариант.
hrie
09.01.2020 11:11Ну браузер и не будет выполнить дохренадцать запросов одновременно, он разобьёт на куски. В этом и суть декларативного подхода — ты ставишь задачу, движок/браузер решает как её выполнить.
Async/await в JS, на мой взгляд, выглядят неидиоматично. Потому что JS — асинхронный язык, и эти куски блокирующего кода смотрятся в нём чужеродно.Zenitchik
09.01.2020 12:09он разобьёт на куски.
Если бы с этим не было багов — мы бы не писали такие редьюсеры.
Cerberuser
10.01.2020 08:05Ну браузер и не будет выполнить дохренадцать запросов одновременно, он разобьёт на куски.
Один раз потребовалось выполнить порядка сотни запросов в произвольном порядке и собрать результаты. Сначала написал этот самый
Promise.all(queries.map(...))
— запросы таки улетели все параллельно и изрядно затормозили страницу. Переписал так, чтобы отправлялось не более пяти за раз — проблема ушла. Так что не всегда всё так хорошо.Zenitchik
10.01.2020 13:25А у меня обычно выходило, что запросы улетают чаще, чем это возможно для сети и сервера, в результате чего ответы приходили только на небольшую их часть. Причём так было всегда, когда я делал Promise.all не к локальным ресурсам.
funca
08.01.2020 22:42Чтобы организовать обработку списка в один проход не надо торопиться делать странные вещи, отказываясь от filter и map.
Например, нужный эффект можно получить с помошью трансдьюсера (есть хорошее интро по теме:
https://www.jeremydaly.com/transducers-supercharge-functional-javascript/ ). В библиотеке lodash, упомянутой в статье, есть похожая фича для filter и map, но с механикой на базе итераторов (chainable methods). В RxJS/IxJS оно так работает по определению.
tushev
08.01.2020 23:14Красиво, но не очень интуитивно понятно, особенно для сторонних разработчиков. А в ряде случаев вообще больше похоже на «смотри как я еще могу». Поэтому я большинстве случаев я бы выбрал более классические методы. По себе я помню лишь несколько случаев когда reduce() действительно органично вписался в задачу.
cup_of_tea_1
09.01.2020 18:51Обожаю reduce, но с ним надо знать меру. Это я вам ответственно заявляю. Иногда лучше заюзать map и filter по отдельности, чем городить один большой и сложный reduce. Но в целом, это очень могущественная штука. Кол-во задач, которые можно им решить, огромно. Но повторюсь, надо себя контролировать и помнить, что мы пишем код не только для себя, но и для других. Тогда и не будет холиваров в стиле reduce vs for
polearnik
во втором примере не уверен что лучше использовать Array.reduce() как раз связка из filter и map более наглядна.
sshikov
Не уверен, что в общем случае такое вообще возможно. И еще reduce как однопроходная вероятно будет сильно оптимальнее с точки зрения ресурсов (во всяком случае в типичном для конкретно js случае). Нагляднее (когда это возможно) — наверное да.
TheShock
Оптимальнее — очевидно обычный цикл. Декларативнее — отдельные filter+map
sshikov
>Оптимальнее — очевидно обычный цикл
Я имел в виду выбор между reduce в один проход, и filter+map в несколько (с той же оговоркой, что это вообще можно будет сделать). Цикл — да, наверное можно написать оптимальнее. Но и понять потом будет сложнее.
TheShock
Та я понял что вы имели ввиду. Редьюс всегда понять тяжелее всего.
VS