iterity v1.0.0
Iterity
Iterity is a library for easy and predictable work with iterable data structures.
Russian version | Версия на русском 🧶
🧗♂️ Motivation
Iterators are a universal interface for working with various types of collections. It allows us to iterate through collections without thinking of specific data structures.
With iterators, we can apply multiple transformations to a collection in one iteration. Let's look at an example. The following code shows work with an array and its methods:
const isActive = (sign) => sign.isActive;
const mapStateToStatus = (sign) => statuses[sign.state];
const uniqueStatuses = signatures.filter(isActive).map(mapStateToStatus);The filter and map methods iterates through the original array and return a new array. Here's how this problem can be solved using iterators and the Iterity library:
const uniqueStatuses = from(signatures).pipe(
filter(isActive),
map(mapStateToStatus)
);The code remains simple and declarative, but at the same time we got a number of advantages:
- The
signaturescollection does not have to be an array: we could useSet,LinkedList,BSTor any other data structure that implements the[Symbol.iterator]method. - Transformations will be applied only when they are needed, that is, when we start iterate through collection.
- If the collection iteration is interrupted, for example, with
breakstatement, then transformations are not applied to the remaining elements. - It is possible to use an infinite collection.
💡 Ideology
The Iterity library API is inspired by the RxJS library. Iterity provides containers for working with iterable objects, as well as functions for their transformation.
Iterity divides collections into synchronous and asynchronous. Synchronous collections have the Symbol.iterator method, and asynchronous collections have the Symbol.AsyncIterator method. In addition, each collection can be resumed: if iteration was interrupted by the break statement, then it can be continued in the future.
Iterity provides two containers — Collection and AsyncCollection. By default, the iterators of both containers are disposable. Both containers provide methods:
pipeto create a composition of iterators.collectto convert the container to an arbitrary type:number | string | boolean | [], etc. This method, naturally, causes a collection iteration.switchto change the container type, for example fromAsyncCollectiontoCollection.toResumableto cast the iterator to resumable type.toDisposableto cast the iterator to disposable type.
The pipe method is the heart of the container. It allows us to create a composition of functions that determine the behavior of the iterator. With its help, it is easy to describe the chain of transformations and predict what values we will deal with.
The Reversible class is a container for an iterator that can be iterated in reverse order.
🥁 Installation and usage
Using NPM:
npm install --save iterityUsing Yarn:
yarn add iterityBasic usage:
import { from, tap } from 'iterity';
const collection = from([1, 2, 3]).pipe(tap((value) => console.log(value)));🌚 Kind of documentation
Collection
The Collection class is a container for a value to work with as a synchronous iterable collection. It implements the Iterable interface.
The Collection class accepts any value in its constructor. If this value already implements the Iterable interface (it is an array, string, Set, etc.), then it will be put in the container as it is.
If passed value is is not iterable, the class will create an iterator for it, which iterates over only the passed value.
Creating an instance:
const collection = new Collection(1);Methods
The static
toIterablemethod transforms the passed value to iterable type, if it is needed.toIterable<T>(value: Iterable<T> | T): Iterable<T>;The
pipemethod takes functions to transform the iterator, and returns a new instance of theCollectionwith a new value. Each function passed topipeacceptsIterableand should returnIterable:operation<T, R>(iterable: Iterable<T>): IterableIterator<R>;The first function passed to
pipegets an iterator of the value stored in the container.The
collectmethod converts the container to an arbitrary type. It takes a function called «collector», which takes anIterableand returns any value. The result returned by collector will be returned bycollectmethod.collect<R>(collector: (iterable: Iterable<T>) => R): R;This method is used in such cases as calculating the product of all the numbers in the collection, combining all the elements of the collection into a string, and so on.
The
switchmethod is designed to change the container type, for example fromAsyncCollectiontoCollection. If the function passed to the method returns aCollectionorAsyncCollectioninstance, then method returns this instance. Otherwise, new instance of the same class with new value will be returned.switch(switcher: (value: Iterable<T> | T) => T | Iterable<T> | AbstractCollection<T>): AbstractCollection<T>;The
toResumableandtoDisposablemethods allow us to control the iterator's possibility of being continued. ThetoResumablemethod allows us to break the collection iteration, but resume it later from the same position.toDisposabledoes the opposite. Both methods return the same instance. ⚠️ By default, all collections are disposable.
AsyncCollection
The AsyncCollection class is a container for a value to work with as an asynchronous iterable collection. It implements theAsyncIterable interface.
Creating an instance:
const collection = new AsyncCollection(1);The interface and logic of AsyncCollection are similar to the Collection class, but there are a few exceptions:
If the constructor of
AsyncCollectiongets iterable object with synchronous iterator, thenAsyncCollectiontransforms it to asynchronous.Instead of the static
toIterablemethod, theAsyncCollectionclass provides a statictoAsyncIterablemethod, which casts the passed value to an asynchronous iterable type, if it is needed. It is able to transform a synchronous iterator to an asynchronous one.toAsyncIterable<T>(value: AsyncIterable<T> | Iterable<T> | T): AsyncIterable<T>;
Other methods work similarly to the methods of the Collection class, but synchronously. Functions for working with asynchronous collections are usually named with the postfix Async, for example: mapAsync, takeAsync, filterAsync.
Reversible
The Reversible class is designed to contain an iterator that can be iterated in reverse order. It implements the Iterable interface.
It is assumed that an instance of Reversible knows how to efficiently iterate over a collection in reverse order, as the developer specifies this behavior during creation.
To achieve this, the class constructor provides two API options:
- Provide a function that immediately returns a reversed iterator. In this case, the same iterator returned by the provided function will be used.
- Provide two functions, where the first returns the length of the collection, and the second defines how to get a value from the collection at a specific index. In this case, a new iterator will be returned, which sequentially calls the
getItemfunction for all indices, starting from the value returned bygetLengthdown to 0.
Methods
The
reversemethod sets the iterator instance to iterate over elements in reverse order. It returns the currentReversibleinstance.reverse(): Reversible<T>;
⚠️ The reverse modifier function can work with instances of the Reversible class. It calls the reverse method on the provided object if it is an instance of this class.
Functions
Iterity provides sets of functions for working with iterable collections. The functions are divided into groups according to the purposes of their application.
- Collectors. Designed to transform the collection to an arbitrary type. Example: get the average of all the numbers in the collection. Used with the
collectmethod. - Selectors. Designed to select specific values from a collection. Examples: get an iterator for the first 10 elements of the collection, filter the elements of the collection. Used with the
pipemethod. - Modifiers. Designed to modify collections. Example: Map each value of a collection to a different value. Used with the
pipemethod. - Decorators. Designed to add specific functionality, or data to an existing collection. Examples: add index to each element, add a function that will be called for each element. Used with the
pipemethod. - Combiners. Designed to combine multiple collections into one. Used with the
pipemethod.
Iterity also provides a set of helper functions.
😮 You can also write functions like these by yourself! Nothing prevents you from writing the necessary modifier and passing it to the pipe method, just like any collector for the collect method.
🍄 Examples
Example 1: Creating the simplest collection from a primitive value:
const collection = new Collection(1);
for (const number of collection) {
console.log(number); // 1
}Example 2: Creating a 10 random numbers collection.
The helper function from gets any value and returns an instance of the container Collection, or 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);
}Example 3: Creating a 10 random numbers asynchronous collection:
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);
}Example 4: Strings as iterable collections
Strings in JavaScript are also iterable collections, so we can work with them this way. The collect method transforms the collection to an arbitrary value, in this case to a string:
import { from, map, join } from 'iterity';
const uppercaseSeq = from('abcdef')
.pipe(map((letter: string) => letter.toUpperCase()))
.collect(join(''));
console.log(uppercaseSeq); // ABCDEFExample 5: Reverse a reversible collection:
The Reversible class is used to create a collection which can be traversed in reverse order in efficient way.
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]Example 6: Event handling with asynchronous iterator:
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]
}
})();