Изучение различных реализаций 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
вернет это единственное значение.
Надеюсь, это имеет смысл. Опять же, я предпочитаю более функциональный подход, поэтому включил его ниже.
Рекурсивное решение
Заключение
До этого упражнения я не знал, насколько похожа структура этих функций. По сути, каждый просто перебирает список и применяет функцию к каждому элементу. Знание этого невероятно ценно. Это означает, что если я когда-нибудь решу, что мне нужно написать свои собственные декларативные функции для списков - возможно, такой, который еще не существует - я могу быть относительно уверен, что они будут следовать аналогичному процессу.
И я надеюсь, что это осознание подчеркивает ценность подобных вещей. Вы можете использовать эти абстракции снова и снова, но, фактически не реконструируя их самостоятельно, вы никогда не получите этого добавленного контекста или системы отсчета. Не только это, но и это конкретное упражнение также подчеркивает магию функционального программирования: то есть то, как использование функций и композиции высшего порядка может привести к тому, что код будет чистым и простым, но все же сможет выполнять сложные операции.
Таким образом, вы не только изучаете язык, но и знакомитесь с мощными парадигмами.