Как сделать ваши преобразования данных более эффективными с помощью преобразователей

1667512824 kak sdelat vashi preobrazovaniya dannyh bolee effektivnymi s pomoshhyu preobrazovatelej

Гвидо Шмиццо

pI9i70xQN0WqZbqKE01Tbba9kX4KknGHkRNc

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

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

Представьте себе, что у вас есть записи 1 000 000 людей и хотят создать подмножество «имен женщин в возрасте от 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 записей 3 раза!

G7mX8Ht-mfOiYsTYYv-kesUmfmOfrdYVo74O

Конечно, отфильтрованные коллекции немного снизятся, но это все равно довольно дорого.

Однако ключевое понимание состоит в том карта и фильтр можно определить с помощью уменьшить. Давайте реализуем приведенный выше код в терминах уменьшить.

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

Используя этот подход, мы уменьшаем (ха-ха!) количество раз, когда создаем временный массив. Ниже приведена визуализация преобразования при использовании уменьшить подход.

dtnQEQo-2hzZ25uM-ycsYAbh8MCV51WRuvJs

Красиво, верно? Но мы говорили о преобразователях. Где наши преобразователи?
Оказывается, filterReducer и mapReducer мы создали редукционные функции. Мы можем выразить это как:

reducing-function :: result, input -> result

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

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

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

Поэтому мы можем составить любое количество преобразователей с помощью функциональной композиции.

Создание собственных преобразователей

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

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

Используя преобразователи, которые мы создали выше, превратим некоторые числа. Мы будем использовать сочинять функция от 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 hereconst xform = R.compose(double, lowerThan6);
const output = numbers.reduce(xform(concatReducer), []); // [2, 4]

The concatReducer называется функция итератора. Это будет вызвано каждой итерации и будет отвечать за преобразование исходных данных функции преобразователя.

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

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

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

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

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

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

Использование 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 мы можем использовать их карта и фильтр методы Это потому, что Рамда внутренняя уменьшить метод использует протокол Transducer Protocol под капотом

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

Вывод

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

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

Если вы хотите узнать больше об этой теме, я рекомендую следующие статьи:

https://clojure.org/reference/transducers
http://blog.cognitect.com/blog/2014/8/6/transducers-are-coming
https://github.com/cognitect-labs/transducers-js#the-transducer-protocol

?? Если вам понравилась эта статья, нажмите эту кнопку внизу? Для меня это будет очень важно, и это поможет другим увидеть эту публикацию.

Следите за мной, чтобы получать уведомления о более подобных программах.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *