Array.reduce — Переход к функциональности шаг за шагом

Почему море соленое? Простой вопрос, но оставайтесь со мной, потому что я думаю, что круговорот воды — это хорошая аналогия для того, как на самом деле работает метод reduce.

Вкратце, вода (H2O) испаряется с поверхности океанов, образуя облака. Облака проходят над сушей и конденсируются, пока не начнут выпадать осадки в виде дождя/снега. В конце концов, выпавшая вода попадает в реки и начинает свой путь к морю. По пути вода собирает минералы, включая соли, и несет их в море. Когда круговорот начинается снова, минералы остаются позади, и со временем уровень концентрации увеличивается.

Чтобы увидеть, как круговорот воды может помочь нам понять принцип работы reduce, мы должны разбить его на три элемента:

  • Минералы соответствуют элементам в массиве, над которыми мы выполняем reduce.
  • Вода — это параметр аккумулятора или функция обратного вызова редуктора.
  • Океан — это накопитель в форме аргумента, как в начальном, так и в конечном значении.

Итак, давайте приведем это в соответствие с кодом

Пожалуйста, извините за измерения концентрации, они, вероятно, ошибочны, я не химик.

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

const mineralsPerCycle = concentrationSamplesOverYear(24);
console.table(mineralsPerCycle);

function concentrationSamplesOverYear(samples) {
  const interval = (2 * Math.PI) / samples;
  const captureSample = i => 
    ((Math.random() + 7) / 8) * ((Math.cos(i * interval) + 2) / 3);
  return [...new Array(samples)].map((_, i) => captureSample(i));
}
Войти в полноэкранный режим Выход из полноэкранного режима

В console.table будут выведены значения, прежде чем мы их используем. Ниже приведен пример, но у вас будут другие значения.

┌─────────┬─────────────────────┐
│ (index) │       Values        │
├─────────┼─────────────────────┤
│    0    │  0.89801916280756   │
│    1    │ 0.9567662790947499  │
│    2    │ 0.9325939089002321  │
│    3    │ 0.8992754278881672  │
│    4    │ 0.7532231143389726  │
│    5    │ 0.6765845269058688  │
│    6    │ 0.6187743088061717  │
│    7    │ 0.5157538308846997  │
│    8    │ 0.46555646525988514 │
│    9    │ 0.38054565223528175 │
│   10    │ 0.33107496732400704 │
│   11    │ 0.3348125096349211  │
│   12    │ 0.30271050596599436 │
│   13    │ 0.30352471441053985 │
│   14    │ 0.3696661578004031  │
│   15    │ 0.4156042590776569  │
│   16    │ 0.4608111994637522  │
│   17    │  0.53172225574472   │
│   18    │ 0.6594949154650602  │
│   19    │ 0.6714790771824638  │
│   20    │ 0.7728233018044018  │
│   21    │ 0.8208884212567936  │
│   22    │  0.924437922104001  │
│   23    │ 0.9497900622814304  │
└─────────┴─────────────────────┘
Вход в полноэкранный режим Выход из полноэкранного режима

Далее мы смоделируем накопление минералов, как подразумевается в каждой двухнедельной пробе.

let oceanConcentration = 0;
console.log(`
Initial concentration = ${oceanConcentration} mgs/ltr
`);

oceanConcentration = mineralsPerCycle.reduce(
  waterCycle,
  oceanConcentration);

console.log(`
Final concentration = ${oceanConcentration} mgs/ltr
`);

function waterCycle(currentConcentration, cycleConcentration) {
  return currentConcentration + cycleConcentration;
}

/* Output

Initial concentration = 0 mgs/ltr

Final concentration = 14.945932946637733 mgs/ltr

*/
Войти в полноэкранный режим Выйти из полноэкранного режима

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

function waterCycle(currentConcentration, cycleConcentration) {
  const newConcentration = currentConcentration + 
    cycleConcentration;
  console.log(`${cycleConcentration} + ${
    currentConcentration} = ${
    newConcentration}`);
  return newConcentration;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Если мы заменим редуктор WaterCycle на вышеприведенную версию, то увидим, как концентрация «накапливается» с каждым образцом.

0.89801916280756 + 0 = 0.89801916280756
0.9567662790947499 + 0.89801916280756 = 1.85478544190231
0.9325939089002321 + 1.85478544190231 = 2.787379350802542
0.8992754278881672 + 2.787379350802542 = 3.686654778690709
0.7532231143389726 + 3.686654778690709 = 4.439877893029681
0.6765845269058688 + 4.439877893029681 = 5.11646241993555
0.6187743088061717 + 5.11646241993555 = 5.735236728741722
0.5157538308846997 + 5.735236728741722 = 6.2509905596264215
0.46555646525988514 + 6.2509905596264215 = 6.716547024886307
0.38054565223528175 + 6.716547024886307 = 7.097092677121588
0.33107496732400704 + 7.097092677121588 = 7.428167644445595
0.3348125096349211 + 7.428167644445595 = 7.762980154080516
0.30271050596599436 + 7.762980154080516 = 8.06569066004651
0.30352471441053985 + 8.06569066004651 = 8.369215374457049
0.3696661578004031 + 8.369215374457049 = 8.738881532257452
0.4156042590776569 + 8.738881532257452 = 9.154485791335109
0.4608111994637522 + 9.154485791335109 = 9.61529699079886
0.53172225574472 + 9.61529699079886 = 10.14701924654358
0.6594949154650602 + 10.14701924654358 = 10.806514162008641
0.6714790771824638 + 10.806514162008641 = 11.477993239191106
0.7728233018044018 + 11.477993239191106 = 12.250816540995508
0.8208884212567936 + 12.250816540995508 = 13.071704962252301
0.924437922104001 + 13.071704962252301 = 13.996142884356303
0.9497900622814304 + 13.996142884356303 = 14.945932946637733
Войти в полноэкранный режим Выход из полноэкранного режима

Неудивительно, что функция обратного вызова метода reduce (параметр один) называется reducer. Однако смущает одно обстоятельство: обратный вызов называется reducer не потому, что он «сводит» массив из (потенциально) многих элементов к одному значению (это может быть и не так). Он называется reducer, потому что (для каждого элемента массива) он принимает два аргумента (в основном, мы расширим этот момент позже) — аккумулятор и элемент. Затем он уменьшает их до одного значения, чтобы сформировать новый аккумулятор.

Что касается того, сколько параметров ожидает метод Array.reduce, то на самом деле он ожидает до четырех:

  1. Аккумулятор — входящее уменьшенное значение
  2. Элемент — элемент массива, который будет уменьшен
  3. Индекс элемента массива (часто не используется)
  4. Обрабатываемый массив (не уменьшаемый), используется очень редко.

В следующем разделе мы рассмотрим тот факт, что выход может быть не единственным значением.

Reduce, корень многих методов

Метод reduce способен выполнять множество операций (мы рассмотрим это позже), и, освоив его, легко найти возможности для его использования, но обычно есть лучшие варианты.

Метод map

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

Если мы используем map следующим образом,

function celsiusToFahrenheit(degCelsius) {
   return (degCelsius * 9) / 5 + 32;
}

console.table([-40, 0, 16, 100].map(celsiusToFahrenheit));
Войти в полноэкранный режим Выйти из полноэкранного режима

на консоли будет представлена таблица температур по Фаренгейту для каждой из температур по Цельсию во входном массиве.
Это также можно записать с помощью метода reduce следующим образом, используя ту же функцию отображения.

console.table([-40, 0, 16, 100].reduce((acc, celsius) =>
   [...acc, celsiusToFahrenheit(celsius)], []));
Вход в полноэкранный режим Выйти из полноэкранного режима

Метод filter

Мы можем сделать нечто подобное для воспроизведения метода filter, используя функцию предиката, например:

const greaterThanFifty = (value) => value > 50;

console.table([20, 40, 60, 80, 100].filter(greaterThanFifty));
// 60, 80, 100
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь с методом reduce.

console.table([20, 40, 60, 80, 100].reduce((acc, val) =>
   greaterThanFifty(val) ? [...acc, val] : acc, [])); 
Ввести полноэкранный режим Выйти из полноэкранного режима

В обоих примерах использование reduce приводит к более длинному и немного более сложному решению. Однако reduce может объединить обе операции за один проход.

console.table(
    [-40, 0, 16, 100].reduce((acc, celsius) => {
        const fahrenheit = celsiusToFahrenheit(celsius);
        return greaterThanFifty(fahrenheit) ? 
            [...acc, fahrenheit] : acc;
    }, [])
); // [60.8, 212]
Вход в полноэкранный режим Выход из полноэкранного режима

На самом деле результатом reduce даже не обязательно должен быть массив.

console.table(
    [-40, 0, 16, 100].reduce(
        (acc, celsius) => ({ ...acc, [celsius]: 
            celsiusToFahrenheit(celsius) }),
        {}
    )
); // {'16': 60.8, '100': 212}
Войти в полноэкранный режим Выход из полноэкранного режима

В приведенном выше примере будет создан объект, содержащий отображение температуры по Цельсию на температуру по Фаренгейту, но только для тех температур по Фаренгейту, которые больше 50 градусов.

До и после

В этом заключительном разделе я хотел бы дать несколько советов по использованию метода reduce.

Что делать

  1. Используйте reduce при преобразовании массива в другую структуру данных.
  2. Рассмотрите возможность использования метода reduce, когда операция представляет собой комбинацию mapping и filtering.

Не рекомендуется

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

Злой близнец Reduce

Метод reduce вряд ли будет использоваться вами каждый день, но знание о его существовании и возможностях добавит еще один инструмент в ваш инструментарий.

Еще менее используемый метод работы с массивами в (не очень) злом близнеце reduce reduceRight, который, я думаю, достаточно очевиден, что он делает. Reduce обрабатывает элементы в массиве слева направо (в порядке индексов), reduceRight обрабатывает массив справа налево (в обратном порядке индексов). Но reduceRight не эквивалентен Array.reverse().reduce(), потому что третий параметр функции reducer будет уменьшаться, а не увеличиваться по мере прохождения метода по массиву.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *