Преобразователи В Js – Так Ли Они Нужны?

Функциональный подход постепенно проник почти во все современные языки программирования.

В то время как некоторые элементы оттуда, вроде монад («просто моноид в категории эндофункторов, в чем проблемаЭ») весьма спорны для мейнстрима, другие, вроде преобразований Map, Reduc, Filter, стали стандартом де-факто.



Преобразователи в JS – так ли они нужны?

При всех своих преимуществах святая троица map/filter/reduce в JS не очень экономно работает с памятью.

Грандиозный архитектурный костыль – преобразователи — успешно портирован с Clojure на JS, и поражает неофитов своей непонятностью, при этом вроде бы решая проблему с чрезмерным выделением памяти.

При чтении документации по Преобразователи.

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).

Главное, чтобы обертываемая функция имела стабильное количество аргументов (нет аргументов со значениями по умолчанию).

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



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

Вместе с данным постом часто просматривают: