Сравните порядок map→filter и filter→map: при каком условии результат не изменится?

Сравните порядок map→filter и filter→map: при каком условии результат не изменится?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

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

Исторический контекст

Функции map и filter появились как выразительный способ описывать преобразование последовательностей без ручного управления индексами и промежуточным состоянием. Такой стиль отделяет операцию над элементом от механизма обхода коллекции.

Композиция этих операций удобна, но порядок вызовов является частью смысла вычисления. Две цепочки с одинаковыми функциями не обязательно эквивалентны, если меняется порядок преобразований.

Постановка проблемы

Представим список цен, где сначала нужно применить скидку, а затем оставить товары дороже определённого порога. Если сначала отфильтровать исходные цены, порог будет применён до скидки, что означает уже другое бизнес-правило.

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

Подробное решение

Пусть map задаёт преобразование f, а filter проверяет условие P. Порядок map → filter оставляет элементы, для которых истинно P(f(x)), и возвращает f(x). Порядок filter → map оставляет элементы, для которых истинно P(x), и только потом возвращает f(x).

Результаты совпадают, если для каждого исходного элемента условие P(x) имеет тот же результат, что и P(f(x)), либо если предикат после перестановки был корректно изменён. Дополнительно преобразование не должно менять порядок элементов или создавать побочные эффекты, влияющие на результат.

let prices = [80, 120] let discountedThenFiltered = prices .map { $0 * 0.8 } .filter { $0 >= 100 } let filteredThenDiscounted = prices .filter { $0 >= 100 } .map { $0 * 0.8 }

В первом варианте после скидки обе цены становятся меньше порога, поэтому результат пуст. Во втором сначала проходит исходная цена 120, а после скидки результатом становится 96. Это разные бизнес-смыслы, хотя используются те же две операции.

Для обычных коллекций map сохраняет количество элементов и их относительный порядок, а filter может уменьшить количество элементов, сохраняя порядок оставшихся. Если преобразование меняет тип элемента или удаляет информацию, обратная перестановка может быть не только семантически неверной, но и невозможной.

Ситуация из практики

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

Первый вариант проще, но реализует правило «исходная цена не ниже 100» и может показать товар с финальной ценой 80. Второй вариант точнее соответствует требованиям, однако требует явно определить финальную цену до проверки порога.

Был выбран второй вариант: map → filter, поскольку условие относится именно к преобразованному значению. Результат стал соответствовать отображаемой пользователю цене, а порядок операций был закреплён тестом на товаре, пересекающем порог после скидки.

Что кандидаты часто упускают

  1. Всегда ли достаточно проверить, что map не меняет значение, влияющее на предикат?

Нет. Нужно учитывать не только конкретное поле, но и саму структуру результата. Если map меняет тип элемента, удаляет информацию или меняет порядок элементов внутри составного значения, эквивалентность может нарушиться. Безопасность перестановки определяется условием P(x) == P(f(x)) для каждого элемента и сохранением требуемого порядка.

  1. Влияет ли ленивое выполнение на то, эквивалентны ли эти порядки?

Нет, ленивость не делает разные условия фильтрации одинаковыми. Она меняет момент вычисления и способ потребления элементов, но map → filter по-прежнему проверяет преобразованные значения, а filter → map — исходные. Ленивый вариант может снизить число реально выполненных преобразований, однако семантика порядка сохраняется.

  1. Почему побочные эффекты в замыканиях делают перестановку опасной даже при одинаковых значениях?

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