Зарегистрируйтесь, чтобы продолжить обучение

Свертка JS: Последовательности

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

Количество заголовков

import {
  node, append, make
} from 'hexlet-html-tags';

const html1 = make();
const html2 = append(html1, node('h1', 'scheme'));
const html3 = append(html2, node('p', 'is a lisp'));

const html4 = append(html3, node('h1', 'haskell'));
const html5 = append(html4, node('p', 'pure'));
const html6 = append(html5, node('h2', 'php'));

headersCount('h1', html6); // 2

В этом примере решается задача подсчёта кол-ва заголовков. Вообще говоря, можно считать количество всего, чего угодно. Но в данном случае, это может быть полезно вот для чего.

Мы уже говорили, что в HTML заголовок h1 на странице может быть только один. Представьте, что мы строим валидатор, который проверяет наш HTML на качество и соответствие стандартам. Одной из проверок будет проверка на количество заголовков первого уровня. Если их больше одного, мы можем выдать предупреждение, что так делать нельзя.

В примере видно, что мы собираем HTML, в котором два заголовка h1. Мы применяем функцию headersCount, которая первым параметром принимает имя тега (можно было оставить только номер заголовка) и, собственно, HTML. В итоге в данной ситуации она выдаст значение 2.

Внутри функция устроена так:

import  {
  l, isEmpty, is, head, tail
} from 'hexlet-pairs-data';

export const headersCount = (tagName, elements) => {
  const iter = (items, acc) => {
    if (isEmpty(items)) {
      return acc;
    }

    const item = head(items);
    const newAcc = is(tagName, item) ? acc + 1 : acc;
    return iter(tail(items), newAcc);
  };
  return iter(elements, 0);
};

Поскольку она выдаёт какой-то конечный результат (скалярный), то понятно, что это другой тип преобразования. В отличие от map и filter нам нужно делать так называемую свёртку, когда все элементы перебираются, происходит общее вычисление, и в конце мы получаем новый результат, который не является отображением или фильтрацией предыдущего списка. Эту задачу можно решить с помощью итеративного процесса, используя внутреннюю функцию iter или цикл. Итак, давайте посмотрим, что здесь происходит.

Как обычно, при итеративном процессе мы вызываем iter(elements, 0) и передаём туда аккумулятор и наши элементы. Начальное значение аккумулятора равно 0, потому что наша функция считает количество.

Дальше внутри функции iter всё происходит, как обычно. Мы берем "голову", вычисляем новый аккумулятор. Здесь есть проверка is: соответствует ли переданное имя тега текущему элементу (в нашем случае, это заголовки). И если это так, то аккумулятор обновляется acc + 1. Если текущий элемент не является заголовком, то newAcc становится равным acc, т.е. не происходит никаких изменений.

После этого мы рекурсивно вызываем функцию iter, передавая в неё "хвост" — tail(items) (остаток наших элементов) и новый аккумулятор. Таким образом итерация за итерацией аккумулятор накапливается и в самом конце, когда items окажется пустым, то наружу вернётся значение нашего аккумулятора.

Свёртка

Фильтрация

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

Особенности

  • Reduce может заменить map/filter.
  • Обычно последняя функция в цепочке преобразований.
  • Её часто называют fold.
  • Внутренняя реализация возможна только через итеративный процесс.

reduce может заменить map/filter, но придётся делать reverse, чтобы элементы были возвращены в правильном порядке.

reduce обычно последняя функция в цепочке преобразований. После map и filter в конечном итоге может применятся reduce для получения какого-то результата. Это не всегда так, но чаще всего.

reduce часто называют fold в зависимости от языка. В некоторых языках встречаются и другие названия. Например, в Ruby - это inject.

Внутренняя реализация возможна только через итеративный процесс, потому что сам reduce реализует его из-за того, что явно используется аккумулятор.

Количество заголовков

import {
  node, append, make, reduce
} from 'hexlet-html-tags';

const html1 = append(make(), node('h1', 'header1'));
const html2 = append(html1, node('h1', 'header2'));
const html3 = append(html2, node('p', 'content'));

reduce((element, acc) => {
  return is('h1', element) ? acc + 1 : acc;
}, 0, html3); // 2

Давайте теперь рассмотрим использование редьюса как концепции и отдельной функции. Мы, как обычно, создаём HTML и дальше вызываем reduce, который принимает на вход уже три элемента. Это сильно отличается от map или filter. Первый элемент — это функция, второй — аккумулятор. Он сильно зависит от того, какую операцию мы собираемся делать. Если бы нам понадобилось сформировать список, то начальным знанием был бы пустой список. Третьим параметром мы передаём сами элементы, которые будем обрабатывать.

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

Та функция, которая непосредственно делает всю обработку в отличие фильтра и мапа принимает на вход два параметра. Первый — это текущий элемент, с которым мы работаем, а второй — это аккумулятор, потому что только мы знаем, как хотим его изменять. Всё, что делает внутренняя реализация переданной функции: мы проверяем, является ли текущий элемент заголовком h1, и если да, то возвращаем аккумулятор, увеличенный на единицу, если нет, то возвращается сам аккумулятор.

В конечном итоге эта функция считает нам количество заголовков.

Ниже еще несколько примеров использования reduce при работе со списками:

import { l, cons, reduce, toString, head } from '@hexlet/pairs-data';

const list = l(0, -10, 2, 38, 2, -2);
const list2 = reduce(Math.max, head(list), list);
console.log(toString(list2)); // => 38

const list3 = reduce((item, acc) => item + acc, 0, list);
console.log(toString(list3)); // => 30

https://repl.it/@hexlet/js-sequences-reduce


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

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

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»