zilt
is a TypeScript lazy iterator library. It lets you create iterators with useful utility methods for declarative-style data processing. The library uses ES6 generators under the hood and has no dependencies.
Some great iterator libraries already exist, why create another one?
- To learn about ES6 generators
- To learn about creating, publishing and maintaining open-source libraries
- For fun
npm install zilt
// ESM
import * as zilt from "zilt";
// CommonJS
const zilt = require("zilt");
Let's consider the following exercise: given a non-empty height * width
matrix full of .
's, draw a zigzag pattern of O
's inside the matrix and return it. This is a contrived example, but it helps demonstrate the capabilities of zilt
iterators.
const input = [
[".", ".", ".", ".", ".", ".", ".", ".", "."],
[".", ".", ".", ".", ".", ".", ".", ".", "."],
[".", ".", ".", ".", ".", ".", ".", ".", "."],
[".", ".", ".", ".", ".", ".", ".", ".", "."],
];
const output = [
["O", ".", ".", "O", ".", ".", "O", ".", "."],
["O", ".", "O", "O", ".", "O", "O", ".", "O"],
["O", "O", ".", "O", "O", ".", "O", "O", "."],
["O", ".", ".", "O", ".", ".", "O", ".", "."],
];
To visit the cells that need to be replaced with an O
, we want to iterate over matrix positions starting from [0, 0]
moving either down or to the top right at each iteration.
With [ystep, xstep]
denoting a vertical ystep
move + a horizontal xstep
move, the step algorithm can be described as follows:
height - 1
vertical steps down ([ystep, xstep] = [1, 0]
) are taken to reach the bottom of the matrixheight - 1
diagonal steps to the top right ([ystep, xstep] = [-1, 1]
) are taken to reach the top again- Repeat
We can create a zilt
iterator over the infinite step sequence from above using the .stretch() and .cycle() methods.
function draw(matrix, height, width) {
const down = [1, 0];
const up = [-1, 1];
const steps = zilt.iter([down, up]) // [down, up]
.stretch(height - 1) // [down * (height - 1), up * (height - 1)]
.cycle(); // repeat the above to infinity
...
}
From there, we just need to initialize some matrix coordinates to [0, 0]
and apply each directional step to it in order to loop over the relevant cells and overwrite their content with O
's. Since the iterator loops indefinitely, we need to make sure to stop the drawing loop once the current coordinate is out of the matrix bounds.
function draw(matrix, height, width) {
const down = [1, 0];
const up = [-1, 1];
const steps = zilt
.iter([down, up])
.stretch(height - 1)
.cycle();
let [y, x] = [0, 0];
for (const [ystep, xstep] of steps) {
if (x >= width) break;
matrix[y][x] = "O";
[y, x] = [y + ystep, x + xstep];
}
return matrix;
}
I would advise against writing code in this style, but here's another way of solving this problem using zilt
exclusively.
function draw(matrix, height, width) {
const down = [1, 0];
const up = [-1, 1];
const steps = zilt
.iter([down, up])
.stretch(height - 1)
.cycle();
zilt.once([0, 0])
.chain(steps)
.accumulate(([y, x], [ys, xs]) => [y + ys, x + xs])
.takeWhile(([_, x]) => x < width)
.forEach(([y, x]) => (matrix[y][x] = "O"));
return matrix;
}
Creates an iterator from an iterable.
// [0, 1, 2]
zilt.iter([0, 1, 2]).collect();
Creates an iterator over a single value.
// [['hello']]
zilt.once(["hello"]).collect();
Creates an iterator over a range of numbers (end excluded).
zilt.range().collect(); // [0, 1, ...] (infinite)
zilt.range(1, 4).collect(); // [1, 2, 3]
zilt.range(3, 0).collect(); // [3, 2, 1]
zilt.range(4).collect(); // [0, 1, 2, 3]
zilt.range(-4).collect(); // [0, -1, -2, -3]
Creates an iterator that yields the values of each passed iterable in sequence.
// [0, 1, 'foo']
zilt.chain([0, 1], ["foo"]).collect();
Creates an iterator over n-tuples from "merging" n iterators together.
// [[0, 6, "foo"], [1, 7, "bar"]]
zilt.zip([0, 1], [6, 7], ["foo", "bar"]).collect();
Consumes the iterator to collect its values in an array and returns it.
// [0, 1, 2]
zilt.range(0, 3).collect();
Consumes the iterator, invoking the provided function for each element.
// prints 0, 1, 2
zilt.range(0, 3).forEach((n, i) => console.log(n));
Partially consumes the iterator and returns the first element for which the predicate is true. Returns undefined if none was found.
// 6
zilt.iter([7, 11, 3, 6, 5]).find((n) => n % 2 === 0);
Partially consumes the iterator and returns the index of the first element for which the predicate is true. Returns undefined if none was found.
// 3
zilt.iter([7, 11, 3, 6, 5]).position((n) => n % 2 === 0);
Consumes the iterator to produce a single value using a given function.
// 6
zilt.range(0, 4).reduce((acc, n) => acc + n);
// 7
zilt.range(0, 4).reduce((acc, n) => acc + n, 1);
// '0123'
zilt.range(0, 4).reduce((acc, n) => acc + n.toString(), "");
Consumes the iterator and returns the number of elements that match a predicate.
// 3
const arr = [10, 15, 15, 20];
zilt.iter(arr).count(); // 4
zilt.iter(arr).count((n) => n === 15); // 2
Partially consumes the iterator and returns its nth element (0-indexed).
zilt.iter([1, 2, 3]).nth(2); // 3
Partially consumes the iterator and returns its first element.
zilt.iter([1, 2, 3]).first(); // 1
Consumes the iterator and returns its last element.
zilt.iter([1, 2, 3]).last(); // 3
Consumes the iterator and returns the element for which the getKey
function result is the minimum.
zilt.iter([3, 6, 4, 1, 8]).min((n) => n); // 1
zilt.iter([3, 6, 4, 1, 8]).min((n) => -n); // 8
Consumes the iterator and returns the element for which the getKey
function result is the maximum.
zilt.iter([3, 6, 4, 1, 8]).max((n) => n); // 8
zilt.iter([3, 6, 4, 1, 8]).max((n) => -n); // 1
Consumes the iterator and returns true if any element satisfies the predicate.
zilt.iter([1, 1, 2]).some((n) => n === 2); // true
zilt.iter([1, 1, 1]).some((n) => n === 2); // false
Consumes the iterator and returns true if every element satisfies the predicate.
zilt.iter([1, 2, 2]).every((n) => n === 2); // false
zilt.iter([2, 2, 2]).every((n) => n === 2); // true
Consumes an iterator over n
-tuples and returns n
arrays.
// [[0, 1, 2], [3, 4, 5]]
zilt.iter([
[0, 3],
[1, 4],
[2, 5],
]).unzip();
Consumes the iterator and returns a pair of arrays.
- the first array contains all elements for which the predicate is true
- the second array contains all elements for which the predicate is false
// [[2, 4], [1, 3]]
zilt.iter([1, 2, 3, 4]).partition((n) => n % 2 === 0);
Consumes the iterator.
// void
zilt.range(0, 3).consume();
Creates an iterator yielding values with an index counter starting from 0.
// [[4, 0], [5, 1], [6, 2]]
zilt.iter([4, 5, 6]).enumerate().collect();
Creates an iterator which uses a callback to determine if an element should be yielded.
// [1, 3]
zilt.range(0, 4)
.filter((n) => n % 2 === 1)
.collect();
Creates an iterator that transforms each value in the original iterator using the passed function parameter.
// [0, 2, 4, 6]
zilt.range(0, 4)
.map((n) => n * 2)
.collect();
Creates an iterator which flattens nested array elements up to a certain depth (maxDepth
).
NOTE: this method only accepts number literals for the
maxDepth
parameter in order to correctly infer the output iterator element type.
const array = [
[0, 1],
[2, [3]],
];
// [0, 1, 2, [3]]
zilt.iter(arr).flat(1).collect();
// [0, 1, 2, 3]
zilt.iter(arr).flat(2).collect();
Creates an iterator which is equivalent to .map().flat(1)
// [1, -1, 2, -2]
zilt.iter([1, 2])
.flatMap((n) => [n, -n])
.collect();
Creates an iterator which only keeps the first num
values.
// [0, 1, 2]
zilt.range(0, 6).take(3).collect();
Creates an iterator which yields values until a predicate is false.
// [0, 1, 2]
zilt.range(0, 6)
.takeWhile((n) => n < 3)
.collect();
Creates an iterator which skips the first num values.
// [3, 4, 5]
zilt.range(0, 6).skip(3).collect();
Creates an iterator which skips values while a predicate is true.
// [3, 4, 5]
zilt.range(0, 6)
.skipWhile((n) => n < 3)
.collect();
Creates an iterator which only yields elements from start to end (excluded). It is equivalent to .skip(start).take(end - start)
.
// [1, 2]
zilt.iter([0, 1, 1, 2, 3]).slice(2, 4).collect();
Creates an iterator that yields values by steps of step
starting from the first element.
// [1, 4, 7]
zilt.range(1, 10).step(3).collect();
Creates an iterator that extends the current iterator with the values of each passed iterable in sequence.
// [0, 'foo', 'bar']
zilt.iter([0]).chain(["foo"], ["bar"]).collect();
Creates an iterator that repeats the current iterator count times. count defaults to Infinity.
// [1, 2, 3, 1, 2, 3]
zilt.range(1, 4).cycle(2).collect();
// [1, 2, 3, 1, 2, 3, 1, 2]
zilt.range(1, 4).cycle().take(8).collect();
Creates an iterator that repeats each value of the current iterator count times.
// [1, 1, 2, 2, 3, 3]
zilt.iter([1, 2, 3]).stretch(2).collect();
Creates an iterator which repeats the provided iterable for each element in the current iterator. Elements are yielded as pairs.
NOTE: prefer using
.nest(start, end)
instead of.nest(range(start, end))
to avoid the unnecessary buffering of iterable values.
// [[0, 'a'], [0, 'b'], [1, 'a'], [1, 'b']]
zilt.range(2).nest(["a", "b"]).collect();
// [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2]]
zilt.range(2).nest(3).collect();
// [[0, 0], [0, -1], [1, 0], [1, -1]]
zilt.range(2).nest(0, -2).collect();
Creates an iterator over n
-tuples from "merging" n
iterators together.
// [[0, 6, "foo"], [1, 7, "bar"]]
zilt.iter([0, 1]).zip([6, 7], ["foo", "bar"]).collect();
Creates an iterator which yields elements by chunks of k
.
// [[0, 1], [2, 3], [4]]
zilt.iter([0, 1, 2, 3, 4]).chunks(2).collect();
Creates an iterator which yields every consecutive k
-element window.
// [[0, 1], [1, 2], [2, 3], [3, 4]]
zilt.iter([0, 1, 2, 3, 4]).windows(2).collect();
Creates an iterator which updates and yields an accumulator using the provided function (similar to reduce, but yields the accumulator at every step instead of returning the final accumulator value).
// [0, 1, 3, 6, 10]
zilt.range(0, 5)
.accumulate((acc, n) => acc + n)
.collect();
Creates an iterator which filters out duplicate values.
// [0, 1, 2, 3, 4]
zilt.iter([0, 1, 1, 2, 3, 2, 4]).unique().collect();
Creates an iterator which filters out duplicate values using a getKey
function.
// [0, 1, 2, 3, 4]
zilt.iter([0, 1, 1, 2, 3, 2, 4])
.uniqueBy((n) => n)
.collect();
Creates an iterator that invokes a callback on each element before yielding.
// [1, 10, 2, 20, 3, 30]
const out: number[] = [];
zilt.range(1, 4)
.inspect((n) => out.push(n))
.map((n) => n * 10)
.inspect((n) => out.push(n))
.consume();
Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!