Уменьшать

Ах, «уменьшить». Швейцарский армейский нож с итеративными функциями. Известен как «фолд» фанатичным монахам¹. Что он не может сделать со списком? Нет. Есть вещи, для которых его нельзя использовать? да. Но отличный способ развить твердое понимание сильных и слабых сторон чего-либо - это использовать это как свой единственный инструмент для каждой задачи. Так что давай сделаем это. Я собираюсь использовать ES6 для этой статьи, но я также сделаю ссылку на предложения Object.entries и Object.values ES2017, для использования которых вам понадобится такой инструмент, как Babel. (Если вы используете новую версию Chrome, все здесь будет работать в консоли.)

Что такое сокращение?

Чтобы обобщить reduce до его самой простой формы, мы можем определить его сигнатуру типа. Вот упрощенная версия:

(reducer: Function, initialValue: Any, list: Array): Any

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

Reduce «накапливает» свое окончательное возвращаемое значение, вызывая функцию редуктора, которую вы передаете ему на каждой итерации, и передавая ее возвращаемое значение на следующую итерацию. Это возвращаемое значение обычно называют «аккумулятором» или иногда «сокращением». На первой итерации аккумулятор начинает равняться начальному значению, которое мы передаем в reduce. Редуктору передаются аккумулятор и текущий элемент в списке. (Обратите внимание, что функция-редуктор иногда может называться «итерацией», академическим термином для функции, которая вызывается на каждой итерации цикла.)

Вот надуманный пример того, что мы обсуждали до сих пор:

Реализация Reduce

Итак, как бы мы реализовали это в ES6? Что ж, нам нужна функция, которая принимает три аргумента: итерацию, начальное значение для аккумулятора и массив, с которым нужно работать. Мы запишем это как каррированную функцию и поместим самый конкретный аргумент (массив) последним. (С этого момента я рекомендую вам взломать консоль JavaScript в вашем браузере и продолжить работу.)

(Примечание: аргумент «аккумулятор» - это то место, где мы передаем наше начальное значение аккумулятора при использовании этой функции. Именование немного отличается от того, что мы рассмотрели выше, но оно будет иметь смысл по мере продвижения.)

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

Если в массиве остались элементы, мы хотим рекурсивно вызвать нашу функцию, передав те же аргументы, но мы разделим массив, передав первый элемент в нем редуктору (вместе с аккумулятором), а остальные элементы как последний аргумент уменьшения.

При каждой рекурсии мы передаем 1) ту же функцию редуктора снова 2) новое обновленное значение аккумулятора, вычисленное путем вызова редуктора с текущим значением аккумулятора и первым элементом в списке 3) остальной частью список. Если в списке больше нет элементов, мы возвращаем аккумулятор («уменьшение»).

Выделенное жирным шрифтом предложение в приведенном выше абзаце важно, потому что именно здесь и выполняется работа.

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

Ура! Оно работает.

Тривиальные примеры - это весело

Вот еще несколько простых вещей, реализованных с помощью reduce:

Иногда вы можете встретить код, похожий на приведенный выше, за исключением гораздо более краткого:

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

Каждая итерационная функция является частным случаем редукции

Мы даже можем реализовать map и filter с помощью reduce.

Очевидно, это глупо, потому что ...

Иногда есть больше семантических вариантов

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

Точно так же, если собственные методы массива, такие как every, includes, some или all, делают то, что вам нужно. , а затем используйте их, потому что они менее подробны, более информативны и прямолинейны. Такие библиотеки, как immutable.js и lodash, имеют свои собственные встроенные версии этих общих утилит, хотя, конечно, имена могут немного отличаться (например, «все» вместо «каждый»).

О, нет! Как уменьшить объект?

Это просто. Reduce работает с массивами, поэтому вызывайте Object.keys, если вам нужно уменьшить ключи объекта, Object.values, если вам нужно уменьшить значения, или Object.entries, если вам нужно смотреть на оба на каждой итерации.

Реализация MapFunction с помощью Reduce

Вот небольшая новинка для супер-ботаников, которые хотят копать дальше. Допустим, вам нужно создать функцию, которая выполняет map с некоторой заданной логикой итераций. Мы можем использовать наш каррированный reduce, чтобы реализовать эту функцию «map». Мы хотим иметь возможность передать функцию в качестве аргумента в mapFunction, а затем вернуть наш настраиваемый сопоставитель, готовый работать с любым массивом в поле зрения.

Привет! Это было просто. Не могли бы вы передать функции? Боже, это очень вкусное карри.

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

Если вы зашли так далеко и поняли код, то поздравляю. Вы разобрались с некоторыми из фундаментальных строительных блоков функционального программирования. Если вам нравится такой подход, я настоятельно рекомендую ознакомиться с Learn You a Haskell for Great Good.

Теперь, когда вы пострадали

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

А вот небольшой удобный пример прямо из документации MDN:

Уменьшение пустых массивов

Одна важная вещь, которую следует знать о Array.prototype.reduce, заключается в том, что если вы не укажете начальное значение при вызове reduce для пустого массива, будет выдано исключение TypeError.

Заключительное примечание по forEach (TL; DR - использовать только для побочных эффектов.)

Иногда мы думаем, что нам нужно использовать нечистую функцию, например forEach, но на самом деле нам просто нужно создать структуру данных, подобную той, которую мы определили, но немного изменили. Мы можем сделать наш код более безопасным и декларативным, используя вместо него reduce. Например, вы можете использовать расширенный объект в качестве начального значения, например:

const newThing = things.reduce(reducer, { some: 'interesting', stuff: [] })

Или, если вам действительно нужно использовать существующую ссылку на объект в качестве начального значения, и вы хотите избежать побочных эффектов изменения этого объекта в вашем reduce и просто вернуть ту же ссылку на объект обратно воспользуйтесь утилитой глубокого клонирования, например lodash.cloneDeep.

const newThing = things.reduce(reducer, cloneDeep(importantObject))

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

Используйте forEach только в том случае, если вам действительно нужно вызывать внешний побочный эффект на каждой итерации.

Adios

Вот и все! Спасибо за прочтение :-)

PS: Я всегда хотел написать эту статью. За несколько дней до его завершения я обнаружил эту академическую статью Грэма Хаттона, в которой обсуждается та же тема, но на более высоком уровне вознесенного богоподобного волшебства. Если у вас хватит смелости проделать это, это будет впечатляюще: Учебное пособие по универсальности и выразительности складки »

PPS: С любовью посвящается вечной памяти поставщиков упаковки для пурпурных блинов Pankaj.

[1]: F # и Scala делают небольшие различия между reduce и fold, но большинство языков этого не делают, поэтому здесь я буду использовать эти термины как синонимы.