Skip to content

Latest commit

 

History

History
283 lines (185 loc) · 18.7 KB

README.rus.md

File metadata and controls

283 lines (185 loc) · 18.7 KB

Iterity

Iterity — библиотека для удобной и предсказуемой работы с итерируемыми структурами данных.

Версия на английском | English version 🏀

🧗‍♂️ Мотивация

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

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

const isActive = (sign) => sign.isActive;
const mapStateToStatus = (sign) => statuses[sign.state];

const uniqueStatuses = signatures.filter(isActive).map(mapStateToStatus);

Методы filter и map обходят массив и возвращают новый массив. А вот как эта задача решается при помощи итераторов и библиотеки Iterity:

const uniqueStatuses = from(signatures).pipe(
  filter(isActive),
  map(mapStateToStatus)
);

Код остаётся простым и декларативным, но при этом мы получили ряд преимуществ:

  1. Коллекция signatures не обязана быть массивом: мы могли бы использовать Set, LinkedList, BST или любую другую структуру, которая реализует метод [Symbol.iterator].
  2. Преобразования будут применены тогда, когда они потребуются, то есть при переборе коллекции. До тех пор не будет выполнено ни одной итерации.
  3. Если обход коллекции будет прерван, например, через break, то преобразования к оставшимся элементам не применяются.
  4. Есть возможность использовать бесконечную последовательность.

💡 Идеология

API библиотеки Iterity вдохновлен библиотекой RxJS. Iterity предоставляет контейнеры для работы с итерируемыми объектами, а так же функции для их трансформации.

Iterity разделяет коллекции на синхронные и асинхронные. Синхронные коллекции имеют метод Symbol.iterator, а асинхронные — Symbol.asyncIterator. Кроме того, каждая коллекция может быть возобновляемой, то есть, если обход коллекции с использованием итератора был прерван вызовом break, то в дальнейшем его можно будет продолжить.

Iterity предоставляет два контейнера — Collection и AsyncCollection. По-умолчанию итераторы обоих контейнеров невозобновляемы. Оба контейнера предоставляют методы:

  1. pipe для создания композиции итераторов.
  2. collect для преобразования контейнера к произвольному типу: number | string | boolean | [] и т.д. Метод, закономерно, вызывает перебор коллекции.
  3. switch для изменения типа контейнера, например с AsyncCollection на Collection.
  4. toResumable для приведения итератора к возобновляемому типу.
  5. toDisposable для итератора к невозобновляемому типу.

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

Отдельно стоит упомянуть еще один класс — Reversible. Это контейнер для итератора, который можно итерировать в обратном порядке.

🥁 Установка и использование

Через NPM:

npm install --save iterity

Через Yarn:

yarn add iterity

Использование:

import { from, tap } from 'iterity';

const collection = from([1, 2, 3]).pipe(tap((value) => console.log(value)));

🌚 Вместо документации

Класс Collection — контейнер для значения, с которым нужно работать как с синхронной итерируемой коллекцией. Реализует интерфейс Iterable.

Класс Collection принимает любое значение в своём конструкторе. Если это значение уже реализует интерфейс Iterable (является массивом, строкой, Set'ом и т.д.), то оно помещается в контейнер без изменений и итерация будет происходить по его элементам.

Если передано неитерируемое значение, то для него будет создан итератор, который перебирает только переданное значение.

Создание экземпляра класса:

const collection = new Collection(1);
  1. Статический метод toIterable приводит переданное значение к итерируемому типу, если оно таким не является изначально.

    toIterable<T>(value: Iterable<T> | T): Iterable<T>;
  2. Метод pipe принимает функции для преобразования итератора, а возвращает новый экземпляр класса Collection, но уже с новым значением. Каждая функция, переданная в pipe, принимает Iterable и должна возвращать Iterable:

    operation<T, R>(iterable: Iterable<T>): IterableIterator<R>;

    Первая функция, переданная в pipe, получает итератор значения, хранящегося в контейнере.

  3. Метод collect преобразует контейнер к произвольному типу. Он принимает функцию, называемую «коллектор», которая принимает Iterable и возвращает любое значение. Результатом вызова метода collect будет значение, возвращенное коллектором.

    collect<R>(collector: (iterable: Iterable<T>) => R): R;

    Этот метод используется в таких случаях, как расчёт произведения всех чисел коллекции, объединения всех элементов коллекции в одну строку и т.д.

  4. Метод switch предназначен для изменения типа контейнера, например с AsyncCollection на Collection. Если переданная методу функция возвращает контейнер Collection или AsyncCollection, то он возвращает этот контейнер. В противном случае возвращается экземпляр того же класса, но с новым значением.

    switch(switcher: (value: Iterable<T> | T) => T | Iterable<T> | AbstractCollection<T>): AbstractCollection<T>;
  5. Методы toResumable и toDisposable позволяют управлять «возобновляемостью» итератора. Метод toResumable позволяет прервать перебор коллекции, но позже возобновить с той же позиции. toDisposable делает противоположное. Оба метода возвращают тот же экземпляр класса, в контексте которого вызваны. ⚠️ По-умолчанию все коллекции невозобновляемы.

Класс AsyncCollection — контейнер для значения, с которым нужно работать как с асинхронной итерируемой коллекцией. Реализует интерфейс AsyncIterable.

Создание экземпляра класса:

const collection = new AsyncCollection(1);

Интерфейс и логика работы AsyncCollection аналогичны классу Collection, но есть несколько исключений:

  1. Если в AsyncCollection передан итерируемый объект, имеющий синхронный итератор, то AsyncCollection преобразует его в асинхронный.

  2. Вместо статического метода toIterable класс AsyncCollection предоставляет статический метод toAsyncIterable, который приводит переданное значение к асинхронному итерируемому типу, если оно таким не является изначально. В том числе умеет приводить синхронный итератор к асинхронному.

    toAsyncIterable<T>(value: AsyncIterable<T> | Iterable<T> | T): AsyncIterable<T>;

Все остальные методы работают аналогично методам класса Collection, но с поправкой на асинхронность. Функции для работы с асинхронными коллекциями принято именовать с постфиксом Async, например: mapAsync, takeAsync, filterAsync.

Класс Reversible — это контейнер для итератора, который можно итерировать в обратном порядке. Реализует интерфейс Iterable.

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

Для этого конструктор класса предоставляет два варианта API:

  1. Передать функцию, которая сразу возвращает обратный итератор. В этом случае, при создании итератора от коллекции будет возвращён тот же итератор, который возвращает переданная функция.
  2. Передать две функции, где первая возвращает конечное значение длины коллекции, а вторая задаёт то, как получить значение в коллекции по конкретному индексу. В этом случае, при создании итератора от коллекции будет возвращён новый итератор, который последовательно вызывает функцию getItem для всех индексов, начиная от значения, возвращенного из getLength и до 0.
  1. Метод reverse задаёт экземпляру итератор, который перебирает элементы в обратном порядке. Метод возвращает текущий экземпляр класса Reversible.

    reverse(): Reversible<T>;

⚠️ Функция-модификатор reverse умеет работать с экземплярами класса Reversible. Она вызывает метод reverse у полученного объекта, если он является экземпляром этого класса.

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

  1. Коллекторы (collectors). Предназначены для приведения коллекции к типу, отличному от контейнерного. Пример: получить среднее арифметическое всех чисел коллекции. Используются с методом collect.
  2. Селекторы (selectors). Предназначены для выбора определенных значений из коллекции. Примеры: получить итератор для первых 10 элементов коллекции, отфильтровать элементы коллекции. Используются с методом pipe.
  3. Модификаторы (modifiers). Предназначены для изменения коллекций. Пример: преобразовать каждое значение коллекции в другое значение. Используются с методом pipe.
  4. Декораторы (decorators). Предназначены для добавления определенной функциональности, или данных к существующей коллекции. Примеры: добавить каждому элементу его порядковый номер, добавить функцию, которая будет вызвана для каждого элемента при обходе коллекции. Используются с методом pipe.
  5. Комбинаторы (combiners). Предназначены для объединения нескольких коллекций в одну. Используются с методом pipe.

Так же Iterity предоставляет набор функций-хелперов.

😮 А еще вы можете написать такие функции самостоятельно! Ничто не мешает написать нужный модификатор и передать его в метод pipe, так же как и любой коллектор для метода collect.

🍄 Примеры

Пример 1: Создание простейшей коллекции из примитивного значения:

const collection = new Collection(1);

for (const number of collection) {
  console.log(number); // 1
}

Пример 2: Создание коллекции из 10 псевдослучайных чисел.

Вспомогательная функция from получает любое значение и возвращает экземпляр контейнера Collection, или AsyncCollection.

import { from, take } from 'iterity';

function* randomGenerator(min = 0, max = 1) {
  while (true) {
    yield Math.floor(Math.random() * (max - min)) + min;
  }
}

const random = randomGenerator(5, 10);
const collection = from(random).pipe(take(10));

for (const number of collection) {
  console.log(number);
}

Пример 3: Создание асинхронной коллекции из 10 псевдослучайных чисел:

import { from, takeAsync } from 'iterity';

async function* asyncRandomGenerator(min = 0, max = 1) {
  ...
}

const random = asyncRandomGenerator(5, 10);
const asyncCollection = from(random).pipe(takeAsync(10));

for await (const number of asyncCollection) {
  console.log(number);
}

Пример 4: Работа со строками

Так тоже можно, потому что строки в JavaScript тоже являются итерируемыми коллекциями. Метод collect приводит коллекцию к произвольному значению, в данном случае к строке:

import { from, map, join } from 'iterity';

const uppercaseSeq = from('abcdef')
  .pipe(map((letter: string) => letter.toUpperCase()))
  .collect(join('')); // Метод collect производит обход коллекции

console.log(uppercaseSeq); // ABCDEF

Пример 5: Работа с реверсивным итерируемым объектом:

Для создания реверсивного перебираемого объекта используется класс Reversible.

import { Reversible, from, reverse } from 'iterity';

const collection = from(
  new Reversible(
    [1, 2, 3],
    (iterable) => iterable.length,
    (index, iterable) => iterable[index]
  )
).pipe(reverse);

console.log([...collection]); // [3, 2, 1]

Пример 6: Обработка событий с использованием асинхронного итератора:

import { from, mapAsync, enumerableAsync } from 'iterity';

async function* subscribe(element: Element, name: string): AsyncIterableIterator<Event> {
  ...
}

(async function() {
  const extractTarget = (event: Event) => event.target;
  const targets = from(subscribe(document.body, 'click')).pipe(
    mapAsync(extractTarget),
    enumerableAsync
  );

  for await (const target of targets) {
    console.log(target); // [index, HTMLElement]
  }
})();