An open API service indexing awesome lists of open source software.

https://github.com/ricokahler/lazy

A small (~900B gzip), useful set of methods for lazy iteration of iterables.
https://github.com/ricokahler/lazy

iterable iteration iterator lazy lazy-evaluation linq

Last synced: 10 months ago
JSON representation

A small (~900B gzip), useful set of methods for lazy iteration of iterables.

Awesome Lists containing this project

README

          

# @ricokahler/lazy ยท [![codecov](https://codecov.io/gh/ricokahler/lazy/branch/main/graph/badge.svg)](https://codecov.io/gh/ricokahler/lazy) [![github status checks](https://badgen.net/github/checks/ricokahler/lazy)](https://github.com/ricokahler/lazy/actions) [![bundlejs.com](https://badgen.net/bundlephobia/minzip/@ricokahler/lazy)](https://bundlejs.com/?q=@ricokahler/lazy) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)

A small [(~900B gzip\*)](https://bundle.js.org/?q=@ricokahler/lazy@latest), **useful** set of methods for lazy iteration of iterables.

---

- [Why this lazy lib?](#why-this-lazy-lib)
- [Do I even need a lazy lib?](#do-i-even-need-a-lazy-lib)
- [Installation](#installation)
- [Usage](#usage)
- [Method reference](#method-reference)

---

## Why this lazy lib?

1. Small size [(~900B gzipped)](https://bundle.js.org/?q=@ricokahler/lazy@latest)
2. Modern `for...of`-only implementations. (Read the source [here](https://github.com/ricokahler/lazy/blob/main/index.js))
3. Only ships _useful_\* methods
4. 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:

```js
Lazy.from(iterable).reverse(); // ๐Ÿ›‘ not implemented!
```

and

```js
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.

## Do I even need a lazy lib?

In order to take full advantage of the short-circuiting nature of call-by-need/lazy-evaluation, your Lazy expression should either:

1. terminate in short-circuiting method
2. take (via `take` or `takeWhile`) a subset of the iterable

## Installation

```
npm i @ricokahler/lazy
```

## Usage

Both ESM and CJS are supported in this package.

```js
// esm
import Lazy from '@ricokahler/lazy';
```

```js
// common-js
const Lazy = require('@ricokahler/lazy');
```

You can then:

1. chain the methods like arrays:

```js
const result = Lazy.from([1, 2, 3])
.map((x) => x + 1)
.filter((x) => x >= 3)
.first();

console.log(result); // 3
```

2. Or use the static method counterparts:

```js
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
```

## Method Reference

[**Conversion methods**](#conversion-methods)

- [`from`](#lazyfrom--)
- [`to`](#to--)

[**Chainable methods**](#chainable-methods)

- [`map`](#map--)
- [`filter`](#filter--)
- [`scan`](#scan--)
- [`flat`](#flat--)
- [`flatMap`](#flatmap--)
- [`take`](#take--)
- [`takeWhile`](#takewhile--)
- [`skip`](#skip--)
- [`skipWhile`](#skipwhile--)

[**Short-circuiting, terminating methods**](#short-circuiting-terminating-methods)

- [`first`](#first--)
- [`find`](#find--)
- [`includes`](#includes--)
- [`some`](#some--)
- [`every`](#every--)

### Conversion methods

#### `Lazy.from` | [๐Ÿ”](#method-reference)

Takes in any iterable and returns it wrapped in a `Lazy` with chainable `Lazy` methods.

```ts
// static method only
Lazy.from(iterable: Iterable): Lazy
```

Note: this is equivalent to calling `new Lazy(iterable)`

#### `to` | [๐Ÿ”](#method-reference)

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:

```js
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);
}
```

```ts
Lazy.to(fromable: {from: (iterable: Iterable) => TInstance}): TInstance
Lazy.to(constructor: {new (iterable: Iterable) => TInstance}): TInstance
```

### Chainable methods

#### `map` | [๐Ÿ”](#method-reference)

Takes in an iterable and returns an iterable generator that yields the result of the callback function on each item from the input iterable.

```ts
Lazy.map(mapper: (t: T) => R): Lazy
```

#### `filter` | [๐Ÿ”](#method-reference)

Takes in an iterable and returns an iterable generator that yields the accepted elements of the given callback function.

```ts
Lazy.filter(accept: (t: T) => t is R): Lazy
Lazy.filter(accept: (t: T) => t is R): Lazy
```

#### `scan` | [๐Ÿ”](#method-reference)

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.

```ts
Lazy.scan(reducer: (acc: TAcc, t: T) => TAcc, initialAcc: TAcc): Lazy
```

#### `flat` | [๐Ÿ”](#method-reference)

Returns a new iterable with all sub-iterable items yielded into it recursively up to the specified depth.

```ts
Lazy.flat(depth?: TDepth): Lazy>
```

#### `flatMap` | [๐Ÿ”](#method-reference)

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.

```ts
Lazy.flatMap(mapper: (value: T) => R | Iterable): Lazy
```

#### `take` | [๐Ÿ”](#method-reference)

Yields the first `n` items of the given iterable and stops further iteration.

```js
const result = Lazy.from([1, 2, 3, 4, 5]).take(3).to(Array);

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

```ts
Lazy.take(n: number): Lazy
```

#### `takeWhile` | [๐Ÿ”](#method-reference)

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.

```js
const result = Lazy.from([1, 2, 3, 4, 5, 0, 1])
.takeWhile((n) => n <= 2)
.to(Array);
console.log(result); // [1, 2]
```

```ts
Lazy.takeWhile(accept: (t: T) => t is R): Lazy
Lazy.takeWhile(accept: (t: T) => unknown): Lazy
```

#### `skip` | [๐Ÿ”](#method-reference)

Skips over the first `n` items of the given iterable then yields the rest.

```js
const result = Lazy.from([1, 2, 3, 4, 5]).skip(2).to(Array);
console.log(result); // [3, 4, 5]
```

```ts
Lazy.skip(n: number): Lazy
```

#### `skipWhile` | [๐Ÿ”](#method-reference)

Skips over the items while the given callback accepts the current item from the given iterable, then yields the rest.

```js
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]
```

```ts
Lazy.skip(n: number): Lazy
```

### Short-circuiting, terminating methods

#### `includes` | [๐Ÿ”](#method-reference)

Determines whether an iterable includes a certain value using `===` comparison. Short-circuits iteration once the value is found.

```ts
Lazy.includes(t: T): boolean
Lazy.includes(t: T): Promise // if async iterable is provided
```

#### `first` | [๐Ÿ”](#method-reference)

Returns the first item of an iterable or `undefined` if the iterable is done/exhausted.

```ts
Lazy.first(): T | undefined
Lazy.first(): Promise // if async iterable is provided
```

#### `find` | [๐Ÿ”](#method-reference)

Returns the first item accepted by the given callback. Short-circuits iteration once an item is found.

```ts
Lazy.find(accept: (t: T) => t is R): R | undefined
Lazy.find(accept: (t: T) => unknown): T | undefined
Lazy.find(accept: (t: T) => unknown): Promise // if async iterable is provided
```

#### `some` | [๐Ÿ”](#method-reference)

Returns `true` if at least one item accepted by the given callback. Short-circuits iteration once an item is accepted.

```ts
Lazy.some(accept: (t: T) => unknown): boolean
Lazy.some(accept: (t: T) => unknown): Promise // if async iterable is provided
```

#### `every` | [๐Ÿ”](#method-reference)

Returns `true` only if all items are accepted by the given callback. Short-circuits iteration once an item is rejected.

```ts
Lazy.every(accept: (t: T) => unknown): boolean
Lazy.every(accept: (t: T) => unknown): Promise // if async iterable is provided
```