Функциональный подход постепенно проник почти во все современные языки программирования.
В то время как некоторые элементы оттуда, вроде монад («просто моноид в категории эндофункторов, в чем проблемаЭ») весьма спорны для мейнстрима, другие, вроде преобразований Map, Reduc, Filter, стали стандартом де-факто.
При всех своих преимуществах святая троица map/filter/reduce в JS не очень экономно работает с памятью.
Грандиозный архитектурный костыль – преобразователи — успешно портирован с Clojure на JS, и поражает неофитов своей непонятностью, при этом вроде бы решая проблему с чрезмерным выделением памяти.
При чтении документации по Преобразователи.
js (протокол для преобразователей) У меня возникло сильное ощущение дежавю - где-то я видел нечто подобное.
Ага - итераторы и генераторы на MDN ! Все вменяемые браузеры и серверные рантаймы уже умеют их делать (гусары, про Осла молчите!).
Не так давно я экспериментировал с этими вещами, чтобы создать что-то, что могло бы передавать данные в JS без создания промежуточных массивов.
Итак, начнем.
Чтобы было стильно, модно, молодежно, мы пионируем две функции из Интернета:
Длинные комментарии излишни — классическая композиция функций: compose(f, g, k) — это f(g(k(x))).const compose = (.
fns) => x => fns.reduceRight((c, f) => f(c), 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).
Главное, чтобы обертываемая функция имела стабильное количество аргументов (нет аргументов со значениями по умолчанию).
Теперь давайте заново изобретем функцию карты, используя генераторы.
function *_gmap(f, a) {
for (let i of a) yield f(i);
}
const gmap = curry(_gmap);
Код элементарный, пройдите через вход а функция е(а) и выложите ответ там.
Можно назвать как gmap(f, а) , так gmap(f)(а) .
Это дает то, что вы можете сохранить частично примененные gmap(ф) в переменную и при необходимости повторно использовать ее.
Теперь фильтруем: function *_gfilter(p, a) {
for (let i of a)
if (p(i)) yield i;
}
const gfilter = curry(_gfilter);
Вы также можете сразу позвонить - gfilter(f, а) , и в лучших традициях функциональности - gfilter(f)(а) .
Чтобы было проще, еще пара примитивных функций (вдохновленных Lisp): 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;
}
}
}
гхед(а) возвращает первый элемент из ввода, gtail(а) - все, кроме первого.
Ну и небольшой пример того, как все это можно использовать: let values = [3, 4, 5, 6, 7, 8, 9];
const square = x => x * x;
const squareNfirst = compose(ghead, gmap(square));
let x = [.
squareNfirst(values)];
К переменной Икс получит массив из одного элемента.
const moreThan5 = gfilter(x => x > 5);
let xxx = [.
moreThan5(values)];
Общая идея такова: на вход gmap и gfilter можно подать либо массив, либо что-то, реализующее итерируемый протокол — а на выходе тоже будет итератор, который в ES6 можно развернуть в обычный массив через три точки ( пусть x = […squareNfirst(значения)] ).
А как насчет сокращения, спросите вы? Универсального подхода здесь не будет, либо используйте классический [].
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);
жадность(f, i, a) свернёт входящий массив или итератор в одно значение.
Пример: const mixFn = compose(greduce((c, v) => c + v, 0), square, moreThan5);
let yyy = mixFn(values);
Составная функция будет последовательно отсекать из входных данных числа больше 5, затем возводить полученные элементы в квадрат и, наконец, суммировать их с помощью сокращения.
Зачем вся эта суета?
Основным преимуществом обработки с использованием итераторов является низкое потребление памяти.В ходе цепочки преобразований мы перетаскиваем ровно один элемент. Плюс, если в цепочке преобразований встречаются такие функции, как ghead(a), мы испытываем «ленивость», т. е.
то, что не возьмет ghead(), даже не будет вычислено.
Ну и плюс функционально, это сейчас модно :) Я надеюсь, что этот подход поможет вам сэкономить немного памяти при обработке многомегабайтных массивов.
В противном случае нет смысла ездить на велосипеде.
Теги: #js #Функциональное программирование #преобразователи #никто не читает теги #JavaScript
-
Такая Непредсказуемая Поддержка Вконтакте
19 Oct, 24 -
Прогноз Цен На Dram
19 Oct, 24 -
Как Я Изучал Функцию Спроса На Шаровары
19 Oct, 24 -
Прогнозы — Дело Неблагодарное
19 Oct, 24