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)
);
Код остаётся простым и декларативным, но при этом мы получили ряд преимуществ:
- Коллекция
signatures
не обязана быть массивом: мы могли бы использоватьSet
,LinkedList
,BST
или любую другую структуру, которая реализует метод[Symbol.iterator]
. - Преобразования будут применены тогда, когда они потребуются, то есть при переборе коллекции. До тех пор не будет выполнено ни одной итерации.
- Если обход коллекции будет прерван, например, через
break
, то преобразования к оставшимся элементам не применяются. - Есть возможность использовать бесконечную последовательность.
API библиотеки Iterity вдохновлен библиотекой RxJS. Iterity предоставляет контейнеры для работы с итерируемыми объектами, а так же функции для их трансформации.
Iterity разделяет коллекции на синхронные и асинхронные. Синхронные коллекции имеют метод Symbol.iterator
, а асинхронные — Symbol.asyncIterator
. Кроме того, каждая коллекция может быть возобновляемой, то есть, если обход коллекции с использованием итератора был прерван вызовом break
, то в дальнейшем его можно будет продолжить.
Iterity предоставляет два контейнера — Collection
и AsyncCollection
. По-умолчанию итераторы обоих контейнеров невозобновляемы. Оба контейнера предоставляют методы:
pipe
для создания композиции итераторов.collect
для преобразования контейнера к произвольному типу:number | string | boolean | []
и т.д. Метод, закономерно, вызывает перебор коллекции.switch
для изменения типа контейнера, например сAsyncCollection
наCollection
.toResumable
для приведения итератора к возобновляемому типу.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);
-
Статический метод
toIterable
приводит переданное значение к итерируемому типу, если оно таким не является изначально.toIterable<T>(value: Iterable<T> | T): Iterable<T>;
-
Метод
pipe
принимает функции для преобразования итератора, а возвращает новый экземпляр классаCollection
, но уже с новым значением. Каждая функция, переданная вpipe
, принимаетIterable
и должна возвращатьIterable
:operation<T, R>(iterable: Iterable<T>): IterableIterator<R>;
Первая функция, переданная в
pipe
, получает итератор значения, хранящегося в контейнере. -
Метод
collect
преобразует контейнер к произвольному типу. Он принимает функцию, называемую «коллектор», которая принимаетIterable
и возвращает любое значение. Результатом вызова методаcollect
будет значение, возвращенное коллектором.collect<R>(collector: (iterable: Iterable<T>) => R): R;
Этот метод используется в таких случаях, как расчёт произведения всех чисел коллекции, объединения всех элементов коллекции в одну строку и т.д.
-
Метод
switch
предназначен для изменения типа контейнера, например сAsyncCollection
наCollection
. Если переданная методу функция возвращает контейнерCollection
илиAsyncCollection
, то он возвращает этот контейнер. В противном случае возвращается экземпляр того же класса, но с новым значением.switch(switcher: (value: Iterable<T> | T) => T | Iterable<T> | AbstractCollection<T>): AbstractCollection<T>;
-
Методы
toResumable
иtoDisposable
позволяют управлять «возобновляемостью» итератора. МетодtoResumable
позволяет прервать перебор коллекции, но позже возобновить с той же позиции.toDisposable
делает противоположное. Оба метода возвращают тот же экземпляр класса, в контексте которого вызваны.⚠️ По-умолчанию все коллекции невозобновляемы.
Класс AsyncCollection
— контейнер для значения, с которым нужно работать как с асинхронной итерируемой коллекцией. Реализует интерфейс AsyncIterable
.
Создание экземпляра класса:
const collection = new AsyncCollection(1);
Интерфейс и логика работы AsyncCollection
аналогичны классу Collection
, но есть несколько исключений:
-
Если в
AsyncCollection
передан итерируемый объект, имеющий синхронный итератор, тоAsyncCollection
преобразует его в асинхронный. -
Вместо статического метода
toIterable
классAsyncCollection
предоставляет статический методtoAsyncIterable
, который приводит переданное значение к асинхронному итерируемому типу, если оно таким не является изначально. В том числе умеет приводить синхронный итератор к асинхронному.toAsyncIterable<T>(value: AsyncIterable<T> | Iterable<T> | T): AsyncIterable<T>;
Все остальные методы работают аналогично методам класса Collection
, но с поправкой на асинхронность. Функции для работы с асинхронными коллекциями принято именовать с постфиксом Async
, например: mapAsync
, takeAsync
, filterAsync
.
Класс Reversible
— это контейнер для итератора, который можно итерировать в обратном порядке. Реализует интерфейс Iterable
.
Предполагается, что экземпляр Reversible
знает, как наиболее эффективно итерировать по коллекции в обратном порядке, так как при создании разработчик сам указывает, как это следует сделать.
Для этого конструктор класса предоставляет два варианта API:
- Передать функцию, которая сразу возвращает обратный итератор. В этом случае, при создании итератора от коллекции будет возвращён тот же итератор, который возвращает переданная функция.
- Передать две функции, где первая возвращает конечное значение длины коллекции, а вторая задаёт то, как получить значение в коллекции по конкретному индексу. В этом случае, при создании итератора от коллекции будет возвращён новый итератор, который последовательно вызывает функцию
getItem
для всех индексов, начиная от значения, возвращенного изgetLength
и до 0.
-
Метод
reverse
задаёт экземпляру итератор, который перебирает элементы в обратном порядке. Метод возвращает текущий экземпляр классаReversible
.reverse(): Reversible<T>;
reverse
умеет работать с экземплярами класса Reversible
. Она вызывает метод reverse
у полученного объекта, если он является экземпляром этого класса.
Iterity предоставляет наборы функций для работы с итерируемыми коллекциями. Условно, функции разделены на группы по целям их применения.
- Коллекторы (collectors). Предназначены для приведения коллекции к типу, отличному от контейнерного. Пример: получить среднее арифметическое всех чисел коллекции. Используются с методом
collect
. - Селекторы (selectors). Предназначены для выбора определенных значений из коллекции. Примеры: получить итератор для первых 10 элементов коллекции, отфильтровать элементы коллекции. Используются с методом
pipe
. - Модификаторы (modifiers). Предназначены для изменения коллекций. Пример: преобразовать каждое значение коллекции в другое значение. Используются с методом
pipe
. - Декораторы (decorators). Предназначены для добавления определенной функциональности, или данных к существующей коллекции. Примеры: добавить каждому элементу его порядковый номер, добавить функцию, которая будет вызвана для каждого элемента при обходе коллекции. Используются с методом
pipe
. - Комбинаторы (combiners). Предназначены для объединения нескольких коллекций в одну. Используются с методом
pipe
.
Так же Iterity предоставляет набор функций-хелперов.
😮 А еще вы можете написать такие функции самостоятельно! Ничто не мешает написать нужный модификатор и передать его в метод pipe
, так же как и любой коллектор для метода collect
.
const collection = new Collection(1);
for (const number of collection) {
console.log(number); // 1
}
Вспомогательная функция 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);
}
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);
}
Так тоже можно, потому что строки в 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
Для создания реверсивного перебираемого объекта используется класс 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]
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]
}
})();