Изучение различных реализаций map (), filter () и reduce ()

Меня учили писать на JavaScript в императивной манере. Вероятно, это потому, что это все, что вы могли делать, когда я изучал это. Но со временем язык эволюционировал, чтобы сделать возможными всевозможные парадигмы.

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

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

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

карта

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

Что-то вроде [1, 2, 3] в f(x) => x * 2 становится [2, 4, 6].

Простое решение

Это работает и читается, но мне это не нравится по нескольким причинам. Во-первых, мы используем конструкцию цикла. Конструкции цикла не работают, у них неудобный синтаксис (центральная опечатка), и то, что происходит внутри них, сложно проверить. Это простой пример, но представьте себе цикл, который должен выполнять десять действий. Это быстро выходит из-под контроля.

Рекурсия может быть лучшим подходом.

Рекурсивное решение

Здесь функция использует оператор распространения, чтобы отделить первый элемент массива от остальных. Затем, если он не определен, мы заканчиваем и просто возвращаемся. Если нет, мы применяем обратный вызов к текущему элементу, а затем вызываем функцию map для остальных из них. Эта функция будет продолжать вызывать себя, пока не применит обратный вызов ко всем элементам, а затем вернет все. Оператор распространения в строке 11 важен, потому что позже мы хотим получить плоский массив. Без него мы получили бы что-то вроде этого: map [1, 2, 3] превратилось бы в [2, [4, [6, []]]].

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

Фильтр

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

Итак, [‘Be’, ‘Bed', ‘Ga’] в f(x) => x INCLUDES ‘B’ становится [‘Be’, ‘Bed’].

Простое решение

Как и map, в простом решении используется конструкция цикла, поэтому оно мне не нравится. У нас также есть несколько уровней вложенности, что затрудняет чтение кода.

Рекурсивное решение

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

Затем мы вызываем filter по другим пунктам. Это заполнит стек вызовов вызовами фильтрации, каждый из которых работает со следующим элементом в массиве, пока мы не дойдем до последнего (тогда вступает в силу строка 5).

Наконец, мы проверяем, возвращает ли предикат true для этого элемента. Если это так, мы включаем его в возвращаемый массив. Если нет, мы возвращаем другие отфильтрованные элементы.

По той же причине, что и раньше, нам нужно распределить значения.

Уменьшать

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

Итак, [1, 2, 3] в f(x, y) => x + y становится 6. x - текущий элемент в массиве, а y - текущее уменьшенное значение операции.

Простое решение

Мы используем еще один цикл в простом решении. В этом коде current относится к текущему уменьшенному значению. Итак, если мы вызвали reduce со следующими параметрами:

reduce ([1, 2], (element, currentTotal) => currentTotal + element, 0)

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

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

Рекурсивное решение

Заключение

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

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

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