iterity v0.1.0-alpha
Iterity
Iterity — библиотека для удобной и предсказуемой работы с итерируемыми структурами данных.
🧗♂️ Мотивация
Итераторы являются универсальным интерфейсом для работы с различными типами коллекций и позволяют абстрагироваться от конкретных структур данных при обходе коллекций.
Используя итераторы мы можем применять несколько преобразований к коллекции за одну итерацию. Посмотрим на примере. В следующем фрагменте представлена работа с массивом через его методы:
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 — сердце контейнера. Он позволяет составить композицию функций, которые определяют поведение итератора. С его помощью легко описать цепочку преобразований и предсказать, с какими значениями мы будем иметь дело при обходе коллекции.
🥁 Установка и использование
Через 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
Класс 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
Класс 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.
Функции
Iterity предоставляет наборы функций для работы с итерируемыми коллекциями. Условно, функции разделены на группы по целям их применения.
- Коллекторы (collectors). Предназначены для приведения коллекции к типу, отличному от контейнерного. Пример: получить среднее арифметическое всех чисел коллекции. Используются с методом
collect. - Селекторы (selectors). Предназначены для выбора определенных значений из коллекции. Примеры: получить итератор для первых 10 элементов коллекции, отфильтровать элементы коллекции. Используются с методом
pipe. - Модификаторы (modifiers). Предназначены для изменения коллекций. Пример: преобразовать каждое значение коллекции в другое значение. Используются с методом
pipe. - Декораторы (decorators). Предназначены для добавления определенной функциональности, или данных к существующей коллекции. Примеры: добавить каждому элементу его порядковый номер, добавить функцию, которая будет вызвана для каждого элемента при обходе коллекции. Используются с методом
pipe. - Комбинаторы (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, take } from 'iterity';
async function* asyncRandomGenerator(min = 0, max = 1) {
...
}
const random = asyncRandomGenerator(5, 10);
const asyncCollection = from(random).pipe(take(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: Обработка событий с использованием асинхронного итератора:
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]
}
})();