https://habrahabr.ru/post/348170/Функциональный подход потихоньку-полегоньку проник почти во все современные языки программирования. Тогда как одни элементы оттуда, вроде монад («всего лишь моноид в категории эндофункторов, в чем проблема?») – очень спорные для мэйнстрима, другие – вроде преобразований map, reduce, filter – стали стандартом де-факто.
При всех своих плюсах святая троица map/filter/reduce – в JS не очень экономно работает с памятью. Грандиозный архитектурный костыль –
трансдьюсеры – успешно запортирован с Clojure на JS, и поражает неофитов своей непонятностью, при этом вроде как решает проблему с излишним выделением памяти.
При чтении документации на
Transducers.js (протокол для трансдьюсеров) меня не оставляло стойкое чувство дежавю – где-то что-то похожее я видел. Ага –
итераторы и генераторы на MDN! Все вменяемые браузеры и серверные рантаймы уже их умеют (гусары, молчать про Ишака!).
Не так давно я экспериментировал с этими вещами, чтобы построить нечто, что может потоково обрабатывать данные в JS – без создания промежуточных массивов.
Итак – поехали.
Чтобы было стильно, модно, молодежно – спионерим из интернетов две функции:
const compose = (...fns) => x => fns.reduceRight((c, f) => f(c), x)
Пространные комментарии излишни – классическая композиция функций: compose(f, g, k) это f(g(k(x))). Ух, вроде со скобками не слажал.
И вторая (тут у нас про функциональщину, помним?):
const curry = (fn, a = []) => (...args) =>
a.length + args.length >= fn.length
? fn(...a, ...args)
: curry(fn, [...a, ...args]);
Превращает функцию с кучкой аргументов в функцию одного аргумента. Для условного f(a, b) вызов curry(f) вернет функцию g – обертку для f, которую можно вызвать как g(a, b), так и g(a)(b). Главное, чтобы оборачиваемая функция имела стабильное количество аргументов (никаких аргументов со значениями по умолчанию).
Теперь переизобретем функцию map, используя генераторы.
function *_gmap(f, a) {
for (let i of a) yield f(i);
}
const gmap = curry(_gmap);
Код элементарный, проходимся по входу
a функцией
f(a) и ответ выкладываем наружу. Можно вызвать как
gmap(f, a), так и
gmap(f)(a). Что это дает – можно сохранить частично примененный
gmap(f) в переменную, а когда понадобится – переиспользовать.
Теперь фильтрация:
function *_gfilter(p, a) {
for (let i of a)
if (p(i)) yield i;
}
const gfilter = curry(_gfilter);
Тоже можно вызвать как сразу –
gfilter(f, a), так и в лучших традициях функциональщины –
gfilter(f)(a).
Чтоб было проще, еще пара примитивных функций (лиспом навеяло):
function *ghead(a) {
for (let i of a) {
yield i;
break;
}
}
function *gtail(a) {
let flag = false;
for (let i of a) {
if (flag) {
yield i;
} else {
flag = true;
}
}
}
ghead(a) возвращает первый элемент от входа,
gtail(a) – всё, кроме первого.
Ну и небольшой пример, как это всё может быть использовано:
let values = [3, 4, 5, 6, 7, 8, 9];
const square = x => x * x;
const squareNfirst = compose(ghead, gmap(square));
let x = [...squareNfirst(values)];
В переменную
x попадет массив из одного элемента.
const moreThan5 = gfilter(x => x > 5);
let xxx = [...moreThan5(values)];
Общая идея такая – на вход gmap и gfilter можно скормить как массив, так и нечто, реализующее iterable protocol – а на выходе тоже будет итератор, который в ES6 можно развернуть в обычный массив через три точки (
let x = […squareNfirst(values)] ).
А что же reduce, можете спросить? Универсального подхода тут не будет, или использовать классический [].reduce(f, init), или вот так:
function _greduce(f, i, a) {
let c = i;
for(let v of a) c = f(c, v);
return c;
}
const greduce = curry(_greduce);
greduce(f, i, a) свернет входящий массив или итератор в одно значение.
Пример:
const mixFn = compose(greduce((c, v) => c + v, 0), square, moreThan5);
let yyy = [...mixFn(values)];
Функция-композит последовательно отсечет из входа числа больше 5, потом полученные элементы возведет в квадрат, и напоследок просуммирует с помощью reduce.
Зачем вся эта возня?
Главный профит от того, что обработка на итераторах – в маленьком потреблении памяти. При цепочке преобразований у нас протаскивается один элемент ровно. Плюс если в цепочке преобразований встречаются функции типа ghead(a) – у нас появляется «ленивость», т.е. то, что ghead() не возьмет – даже не будет обсчитываться.
Ну и плюс функциональненько, это сейчас модно :)
Надеюсь, что подобный подход поможет вам сэкономить немножко памяти при обработке много-десятко-мегабайтных массивов. Иначе не стоит и велосипедить.