Реализуем map, reduce, filter


С приходом моды на функциональное программирование в JS пришли и вопросы для собеседований по этой же теме. Логично, если назвался функциональщиком, не желая отвечать на вопросы по ООП, будь добр показать, что ты умеешь в FP. В свое время была мода на NoSQL… Одно могу сказать: прошла и теперь бэкенд разработчиков на собеседовании спрашивают как про NoSQL, так и про SQL. И эти вещи сосуществуют, а не взвимно заменяют друг друга.

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

В этой статье покажу пару вопросов, которые в целом интересны своим подходом. Эти задачи интересны сами по себе даже в отрыве от всяких ФП парадигм.

Вопрос 1. Рразогрев

Напишите функции head и tail для массивов. Первая возвращает элемент с индексом 0. Вторая возвращает все элементы массива, кроме нулевого.

const arr = [ 1, 2, 3 ];
// head(arr): 1
// tail(arr): [ 2, 3 ]

Прелесть ES6+ в том, что теперь такие задачи легко решаются с помощью деструктуризации без манипуляций через slice, pop и shift.

Без написания функций мы можем решить задачу, почти не написав код:

const [ head, ...tail ] = [ 1, 2, 3 ];
// head: 1
// tail: [ 2, 3 ]

Соответственно для решения задачи по условию, нам достаточно обернуть этот код в функцию:

const head = ([ head, ...tail ]) => head;
const tail = ([ head, ...tail ]) => tail;

Вопрос 2. Реализуйте функцию map

Условие: реализовать функцию map избегая итераций (for, while), встроенных функций forEach, map, reduce, filter и им подобным.

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

const map = ([ head, ...tail ], fn) =>
tail.length
? [ fn(head), ...map(tail, fn) ]
: [ fn(head) ]
;

Проверяем:

const a = [1,2,3];
const b = map(a, x => x * 2);
console.log(b)
// [2, 4, 6]

Все работает корректно.

Вопрос 3. Реализуйте функцию Reduce

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

const reduce = ([ head, ...tail ], fn, initial) =>
tail.length
? reduce(tail, fn, fn(initial, head))
: fn(initial, head);
;

Тестируем:

const a = [1,2,3];
const b = reduce(a, (a,b) => a+= b, 0);
console.log(b)
// 6

Вопрос 4. Реализуйте функцию filter

Опять же условия те же. Ну тут уже как с фокусами. Если знаешь хотя бы 2–3 техники, считай знаешь 30% реализации всех фокусов на планете.

Реализация filter

const filter = ([ head, ...tail ], fn, newHead=void[]) =>
(
(newHead = fn(head) ? [head] : []),
(tail.length
? [ ...newHead, ...filter(tail, fn) ]
: newHead
)
);

В этих примерах красота и лаконичность кода достигнута благодаря возможностям деструктуризации в JavaScript.