Главная » Хабрахабр » [Перевод] Эффективное преобразование данных с использованием трансдьюсеров

[Перевод] Эффективное преобразование данных с использованием трансдьюсеров

image

Преобразование больших массивов данных может быть достаточно ресурсозатратным, особенно, когда вы используете функции высшего порядка, такие как map и filter.

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

Представьте, что у вас база в 1000000 человек, и вам нужно создать сабсет “имен женщин старше 18 лет, которые живут в Нидерландах”. Существуют различные способы решения этой проблемы, но начну с цепочек.

const ageAbove18 = (person) => person.age > 18;
const isFemale = (person) => person.gender === ‘female’;
const livesInTheNetherlands = (person) => person.country === ‘NL’;
const pickFullName = (person) => person.fullName;
const output = bigCollectionOfData .filter(livesInTheNetherlands) .filter(isFemale) .filter(ageAbove18) .map(pickFullName);

Ниже представлен пример решения задачи с помощью цепочечного подхода, который создает временные массивы. Представьте затраты на тройное прохождение цикла более, чем в 1000000 записей!

image

Конечно, отфильтрованные коллекции будут несколько сокращены, но это, все еще довольно затратно.

Однако главный момент заключается в том, что map и filter могут быть определены с помощью reduce. Давайте попытаемся реализовать приведенный выше код в формате сокращений.

const mapReducer = (mapper) => (result, input) => { return result.concat(mapper(input));
};
const filterReducer (predicate) => (result, input) => { return predicate(input) ? result.concat(input) : result;
};
const personRequirements = (person) => ageAbove18(person) && isFemale(person) && livesInTheNetherlands(person);
const output = bigCollectionOfData .reduce(filterReducer(personRequirements), []) .reduce(mapReducer(pickFullName), []);

И более того, Мы можем еще более упростить сокращение (filterReducer) с помощью композиции функций.

filterReducer(compose(ageAbove18, isFemale, livesInTheNetherlands));

При использовании такого подхода мы уменьшаем (хаха!) количество временных массивов. Ниже представлен пример трансформации при использовании сокращающего подхода.

image

Прелестно, не правда ли? Но мы говорили о трансдьюсерах. Где же наши трансдьюсеры?
Получается, filterReducer и mapReducer, которые мы создали, сокращают функцию. Это можно выразить как:

reducing-function :: result, input -> result

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

transducer :: (result, input -> result) -> (result, input -> result)

Самая интересная часть заключается в том, что трансдьюсеры, примерно симметричны в их сигнатурах типов. Они берут одну сокращающую функцию и возвращают другую.
Следовательно, мы можем составить любое количество редьюсеров с использованием композиции функций.

Построим свои собственные трансдьюсеры.

Надеюсь, все это приобрело теперь более ясный вид. Давайте запилим теперь наши собственные трансдьюсерные функции для map и filter.

const mapTransducer = (mapper) => (reducingFunction) => { return (result, input) => reducingFunction(result, mapper(input));
}
const filterTransducer = (predicate) => (reducingFunction) => { return (result, input) => predicate(input) ? reducingFunction(result, input) : result;
}

Используя редьюсеры, которые мы создали выше, давайте преобразуем некоторые числа. Мы будем использовать функцию compose из библиотеки RamdaJS. RamdaJS -это библиотека, которая содержит практические, функциональные методы и специально разработана для стилей функционального программирования.

const concatReducer = (result, input) => result.concat(input);
const lowerThan6 = filterTransducer((value) => value < 6);
const double = mapTransducer((value) => value * 2);
const numbers = [1, 2, 3];
// Using Ramda's compose here
const xform = R.compose(double, lowerThan6);
const output = numbers.reduce(xform(concatReducer), []); // [2, 4]

Функция “concatReducer” называется функцией итератора. Он будет вызываться на каждой итерации и будет отвечать за преобразование выходной функции трансдьюсера.

В этом примере мы просто конкатенируем результат. Поскольку каждый трансдьюсер принимает только функцию сокращения, мы не можем использовать value.concat.

Когда мы составляем несколько преобразователей в одну функцию, чаще всего это называется xform трансдьюсер. Поэтому, когда вы увидите это где-то, вы будете знать, что это значит.

Составление нескольких преобразователей.

Мы использовали в предыдущем примере обычную композицию функций, и вам может быть интересно каков порядок оценки этого. Хотя композиция функции применяется справа налево, преобразования будут оцениваться слева направо во время выполнения, что будет более интуитивно понятным для тех из нас, кто читает слева направо.

Потребуется немного поразмыслить, чтобы понять, почему это именно так: учитывая, что наш трансдьюсер double возвращает функцию редьюсирования, и lowerThan6 также возвращает функцию редьюсирования, при соединении оных, значение double будет передано в lowerThan6 и в результате получим функцию lowerThan6. Таким образом, double — результат композиции а порядок оценки применяется слева направо.

Я создал пример, так что вы можете взглянуть на это сами.

Использование RamdaJS для оптимизации читабельности.

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

const lowerThan6 = R.filter((value) => value < 6);
const double = R.map((value) => value * 2);
const numbers = [1, 2, 3];
const xform = R.compose(double, lowerThan6);
const output = R.into([], xform, numbers); // [2,4]

Ramda дает возможность использовать ее мапы и фильтры. Это потому, что внутренний редьюсирующий метод Ramda использует встроенный Transducer Protocol.

“Цель этого протокола заключается в том, что во всех JavaScript реализациях трансдьюсера, взаимодействие происходит независимо от поверхностного уровня API. Он вызывает преобразователи независимо от контекста источников их ввода и вывода, и указывает только суть трансформации с точки зрения отдельного элемента.
Поскольку трансдьюсеры отделены от источников их ввода и вывода, они могут быть использованы во многих различных процессах — коллекциях, стримах, каналах, обозревателях и т. д. Преобразователи компонуют напрямую, без привязки к вводу или созданию промежуточных агрегатов”.

Заключение

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

И особенно продуктивно они применяемы при преобразовании больших массивов данных, но вы можете также использовать тот же преобразователь для работы с единичной записью.

Больше информации по данной теме можно найти в следующих статьях

x

Ещё Hi-Tech Интересное!

Финтех-дайджест: финансовым регуляторам нужен ИИ для того, чтобы работать в современных условиях

Сегодня в выпуске: Привет, Хабр! В США считают, что финансовым регуляторам необходимо использовать ИИ; Банки РФ обязали уведомлять ЦБ перед тем, как рассказывать прессе о произошедших хакерских атаках; Биткоин двинулся вперед, есть оптимистичные прогнозы; Несмотря на запрет биткоинов, в Китае ...

[Из песочницы] Delphi и C++Builder Community Edition

Embarcadero представила новую редакцию Delphi и C++ Builder: Delphi Community Edition и C++ Builder Community Edition. Случилось то, чего мы все так долго ждали. Но есть и ограничения — вы не сможете установить одновременно Delphi и C++ Builder на один ...