A small (~900B gzip*), useful set of methods for lazy iteration of iterables.
- Small size (~900B gzipped)
- Modern
for...of
-only implementations. (Read the source here) - Only ships useful* methods
- Support for async iterators
*Useful is a keyword here because this library only ships with iterable methods that can take advantage of chained lazy iteration.
What that boils down to is shipping methods that never have to exhaust the iterable in order to yield the next item.
An example of this would be a reverse()
method. If this lib implemented a reverse()
method, it would have to buffer the items received from the iterable into an array to be able to yield the items in reverse.
Since we have to buffer the items into an array anyway, there's not all that much difference between:
Lazy.from(iterable).reverse(); // 🛑 not implemented!
and
Lazy.from(iterable).to(Array).reverse();
This constraint has some mental model benefits too. If you find yourself frequently needing to convert your iterable to an array, then there's a good chance lazy evaluation is not giving you much benefit.
In order to take full advantage of the short-circuiting nature of call-by-need/lazy-evaluation, your Lazy expression should either:
- terminate in short-circuiting method
- take (via
take
ortakeWhile
) a subset of the iterable
npm i @ricokahler/lazy
Both ESM and CJS are supported in this package.
// esm
import Lazy from '@ricokahler/lazy';
// common-js
const Lazy = require('@ricokahler/lazy');
You can then:
- chain the methods like arrays:
const result = Lazy.from([1, 2, 3])
.map((x) => x + 1)
.filter((x) => x >= 3)
.first();
console.log(result); // 3
- Or use the static method counterparts:
let iterable = Lazy.from([1, 2, 3]);
iterable = Lazy.map(iterable, (x) => x + 1);
iterable = Lazy.filter(iterable, (x) => x >= 3);
const result = Lazy.first(iterable);
console.log(result); // 3
Short-circuiting, terminating methods
Lazy.from
| 🔝
Takes in any iterable and returns it wrapped in a Lazy
with chainable Lazy
methods.
// static method only
Lazy.from<T>(iterable: Iterable<T>): Lazy<T>
Note: this is equivalent to calling new Lazy(iterable)
to
| 🔝
Writes the iterable into another data structure. Accepts an object with a from
method that accepts an iterable (e.g. Array.from
) or a constructor that accepts an iterable.
Note: if used with an async iterable, it will await and buffer all items into an array first.
The implementation is as follows:
function to(iterable, constructorOrFromable) {
// switches behavior for async iterators
if (typeof iterable[Symbol.asyncIterator] === 'function') {
return toAsync(iterable, constructorOrFromable);
}
return toSync(iterable, constructorOrFromable);
}
async function toAsync(iterable, constructorOrFromable) {
// Async iterators will be buffered into an array first
const items = [];
for await (const item of iterable) {
items.push(item);
}
return toSync(items, constructorOrFromable);
}
function toSync(iterable, constructorOrFromable) {
if (typeof constructorOrFromable.from === 'function') {
return constructorOrFromable.from(iterable);
}
return new constructorOrFromable(iterable);
}
Lazy<T>.to<TInstance>(fromable: {from: (iterable: Iterable<T>) => TInstance}): TInstance
Lazy<T>.to<TInstance>(constructor: {new (iterable: Iterable<T>) => TInstance}): TInstance
map
| 🔝
Takes in an iterable and returns an iterable generator that yields the result of the callback function on each item from the input iterable.
Lazy<T>.map<R>(mapper: (t: T) => R): Lazy<R>
filter
| 🔝
Takes in an iterable and returns an iterable generator that yields the accepted elements of the given callback function.
Lazy<T>.filter<R extends T>(accept: (t: T) => t is R): Lazy<R>
Lazy<T>.filter<R extends T>(accept: (t: T) => t is R): Lazy<R>
scan
| 🔝
Takes in an iterable, a reducer, and an initial accumulator value and returns another iterable that yields every intermediate accumulator created in the reducer for each item in the input iterable.
Useful for encapsulating state over time.
Note: the initial accumulator value is required.
Lazy<T>.scan<TAcc>(reducer: (acc: TAcc, t: T) => TAcc, initialAcc: TAcc): Lazy<TAcc>
flat
| 🔝
Returns a new iterable with all sub-iterable items yielded into it recursively up to the specified depth.
Lazy<T>.flat<TDepth extends number>(depth?: TDepth): Lazy<Flattened<T, TDepth>>
flatMap
| 🔝
Calls the result of the given callback function on each item of the parent iterable. Then, yields the result of each into a flatted iterable. This is identical to a map followed by flat with depth 1.
Lazy<T>.flatMap<R>(mapper: (value: T) => R | Iterable<R>): Lazy<R>
take
| 🔝
Yields the first n
items of the given iterable and stops further iteration.
const result = Lazy.from([1, 2, 3, 4, 5]).take(3).to(Array);
console.log(result); // [1, 2, 3]
Lazy<T>.take(n: number): Lazy<T>
takeWhile
| 🔝
Yields while the callback function accepts the current item from the given iterable. Iteration finishes as soon as an item is rejected by the callback.
const result = Lazy.from([1, 2, 3, 4, 5, 0, 1])
.takeWhile((n) => n <= 2)
.to(Array);
console.log(result); // [1, 2]
Lazy<T>.takeWhile<R extends T>(accept: (t: T) => t is R): Lazy<R>
Lazy<T>.takeWhile(accept: (t: T) => unknown): Lazy<T>
skip
| 🔝
Skips over the first n
items of the given iterable then yields the rest.
const result = Lazy.from([1, 2, 3, 4, 5]).skip(2).to(Array);
console.log(result); // [3, 4, 5]
Lazy<T>.skip(n: number): Lazy<T>
skipWhile
| 🔝
Skips over the items while the given callback accepts the current item from the given iterable, then yields the rest.
const result = Lazy.from([1, 2, 3, 4, 5, 0, 1])
.skipWhile((n) => n <= 2)
.to(Array);
console.log(result); // [3, 4, 5, 0, 1]
Lazy<T>.skip(n: number): Lazy<T>
includes
| 🔝
Determines whether an iterable includes a certain value using ===
comparison. Short-circuits iteration once the value is found.
Lazy<T>.includes(t: T): boolean
Lazy<T>.includes(t: T): Promise<boolean> // if async iterable is provided
first
| 🔝
Returns the first item of an iterable or undefined
if the iterable is done/exhausted.
Lazy<T>.first(): T | undefined
Lazy<T>.first(): Promise<T | undefined> // if async iterable is provided
find
| 🔝
Returns the first item accepted by the given callback. Short-circuits iteration once an item is found.
Lazy<T>.find<R extends T>(accept: (t: T) => t is R): R | undefined
Lazy<T>.find(accept: (t: T) => unknown): T | undefined
Lazy<T>.find(accept: (t: T) => unknown): Promise<T | undefined> // if async iterable is provided
some
| 🔝
Returns true
if at least one item accepted by the given callback. Short-circuits iteration once an item is accepted.
Lazy<T>.some(accept: (t: T) => unknown): boolean
Lazy<T>.some(accept: (t: T) => unknown): Promise<boolean> // if async iterable is provided
every
| 🔝
Returns true
only if all items are accepted by the given callback. Short-circuits iteration once an item is rejected.
Lazy<T>.every(accept: (t: T) => unknown): boolean
Lazy<T>.every(accept: (t: T) => unknown): Promise<boolean> // if async iterable is provided