An exercise for fun and profit that turns out to be a usable collection manipulation library
Streamz was a project made purely as a playground to look for ideas and ways to implement actual lazy evaluation of collection manipulations. It turns out this is a fairly usable implementation and could actually make a nice library.
Streamz is mainly inspired by C#’s LINQ, Java 8’s Stream API, Rust’s iter, Kotlin’s sequences, C++’s Range-v3 and finally sequency.
First you need to install it via NPM : npm i -S @voltra/streamz
Streamz does not provide a direct “plug into browser” file yet so you’ll be required to use a build system (if you are targeting browsers).
Once installed, you can simply require/import it :
import { Stream } from "@voltra/streamz"
Streamz was fully developed using typescript, therefore you have access to the entire source code in the src
directory.
The entry point dist/index.js
provides the following :
import {
Stream,
Packer,
KeyGen,
ValueGen,
emptyPayloadValue,
isPayloadValueEmpty,
streamIsValidValue,
invalidStreamIteratorPayload
} from "@voltra/streamz"
Stream
is the class used for collection manipulation, it provides factoriesPacker
is used to transform a Stream
into a collection (e.g. an array, an object, a Map
, a Set
, etc…)KeyGen
is an helper object that contains functions destined to be used in Packer#toObject
and similar methodsValueGen
is an helper object that contains functions destined to be used in Packer#toObject
and similar methodsemptyPayloadValue
is a global Symbol
use to mark invalid payloadsisPayloadValueEmpty
is a function that checks whether or not the given value is one of an invalid payloadstreamIsValidValue
is a function that checks whether or not the given value is valid (not from an invalid payload)invalidStreamIteratorPayload
is a factory for invalid payloadssrc/index.ts
provides the following :
import {
Stream,
Packer,
KeyGen,
ValueGen,
emptyPayloadValue,
isPayloadValueEmpty,
streamIsValidValue,
invalidStreamIteratorPayload,
BaseStreamIterator,
EndStreamIterator,
StreamIteratorPayload
} from "@voltra/streamz/src/index"
BaseStreamIterator
is an interface that represents both first and intermediate iterators in the iterator chain (i.e. operation chain)EndStreamIterator
is an interface that represents the final iterator in the iterator chain (i.e. operation chain)StreamIteratorPayload
is an interface that represents the payload retrieved when calling next
on an iteratorHelper types are provided under src/types
(function.d.ts
and helpers.d.ts
).
import { Stream, KeyGen, ValueGen } from "@voltra/streamz"
Stream.of(1, 2, 3, 4) //Stream{1, 2, 3, 4}
.map(x => x+1) //Stream{2, 3, 4, 5}
.filter(x => x%2) //Stream{3, 5}
.pack.toArray(); //[3, 5]
Stream.fromObject({hello: "world", wombo: "combo"})
.map(([k, v]) => ([`key(${k})`, `value(${v})`]))
.pack.toObject(KeyGen.entries, ValueGen.entries);
//Stream{["hello", "world"], ["wombo", "combo"]}
//Stream{["key(hello)", "value(world)"], ["key(wombo)", "value(combo)"]}
//{"key(wombo)": "value(combo)", "key(hello)": "value(world)"}
Stream.of(1, 2, 1, 1, 2, 1) //Stream{1, 2, 1, 1, 2, 1}
.map(x => x+1) //Stream{2, 3, 2, 2, 3, 2}
.unique() //Stream{2, 3}
.pack.toArray(); //[2, 3]
Stream.of(2, 4, 6, 8) //Stream{2, 4, 6, 8}
.map(x => x+1) //Stream{3, 5, 7, 9}
.all(x => x%2); //true
import { Stream } from "@voltra/streamz"
const X = Stream.of(1, 2, 3, 4)
.map(x => x+1)
.filter(x => x%2)
.pack.toArray();
console.log(X);
//Is roughly equivalent to
const X = [];
for(const it in [1, 2, 3, 4]){
const x = it+1;
if(x % 2)
X.push(x);
}
console.log(X);
A complete JSDoc will be provided once ready.
extend
in src/extend.ts
(also exported in src/index.ts
and therefore in dist/index.js
) :
Array#stream()
is Stream.from(this)
Object#stream()
is Stream.fromObject(this)
Set#stream()
is Stream.fromSet(this)
Map#stream()
is Stream.fromMap(this)
Object.fromStreams
is Stream.zipToObject
Number.range
is Stream.range
Number.infiniteRange
is Stream.infinite
Compare
in src/utils.ts
(also exported in src/index.ts
and therefore in dist/index.js
) :
Compare.asc(lhs, rhs)
standard comparison function (ascending)Compare.desc(lhs, rhs)
standard comparison function (descending)Compare.mapped
“namespace” for mapped comparison functionsCompare.mapped.asc(mapper)
crafts a Compare#asc
where elements were mapped using mapper
Compare.mapped.desc(mapper)
crafts a Compare#desc
where elements were mapped using mapper
Stream
initial operations :
Stream.fromMap(map)
creates a stream from a Map
Stream.fromSet(set)
creates a stream from a Set
Stream
intermediate operations :
Stream#chunked(size = 3)
groups items in chunks whose size is at most size
Stream#zip(stream)
combines this stream (makes a pair using one item from each stream)Stream.zip(lhs, rhs)
same as lhs.zip(rhs)
Stream#zipBy(stream, mapper)
combines this stream using the mapper function to craft the new itemStream.zipBy(lhs, rhs, mapper)
same as lhs.zipBy(rhs, mapper)
Stream#nonNull()
filters out any null
Stream#nonFalsy()
filters out any falsy value (0
, null
, undefined
, etc…)Stream#filterNot(predicate)
filters element that satisfy the predicateStream#filterOut(predicate)
alias for Stream#filterNot(predicate)
Stream#filterIsInstance(class)
filters out elements that are not instance of class
Stream#takeWhile(predicate)
keeps item until predicate
is not satisfiedStream#takeUntil(predicate)
keeps item while predicate
is not satisfiedStream
terminal operations :
Stream#count(predicate = (_ => true))
counts the elements that satisfy the predicateStream#contains(elem)
checks whether or not elem
is an item of this streamStream#atIndex(index)
retrieves the element at the given index (or null if there is none)Stream#atIndexOr(index, default)
retrieves the element or get back default
Stream#first()
retrieves the first element (or null
)Stream#firstOr(default)
retrieves the first element (or default
)Stream#zipToObject(stream)
zips into an object using this
as keys and stream
as valuesStream.zipToObject(keys, values)
equivalent to keys.zipToObject(values)
Stream#unzip()
unzips a stream of pairs into a pair of arraysStream#unzipBy(firstGen, lastGen)
unzips a stream into a pair of arrays applyingfirstGen
for the first element of the pair and lastGen
for the last element of the pairStream#unzipVia(mapper)
convenience method to unzip a stream of pairs into a pair of arrays using a single mapper function that returns a pairStream#sortedWith(comparator)
packs to an array sorted using comparator
as the comparison functionStream#sortedBy(mapper)
maps the objects and sort in ascending order via regular comparison operatorsStream#sortedDescBy(mapper)
same as sortedBy
but in descending orderStream#sorted()
same as sortedBy
but without mappingStream#sortedDesc()
same as sorted
but in descending orderStreamz is not a usual object, it doesn’t really expect you to call its constructor, it provides a lot of static factory methods in order for you to get the most fluent experience possible :
Stream.of(...args)
takes any amount of parameters and makes a stream out of itStream.from(args)
takes an array and makes a stream out of itStream.fromObject(obj)
takes an object and makes a stream out of itStream.range(higher)
takes a number used as its lower bound makes the range [0 ; higher) as a streamStream.range(lower, higher, step = 1)
takes both bounds, the step and makes the range [lower ; higher) as a stream (uses step
as increment instead of 1)Stream.infinite(lower = 0)
makes a stream for the range [lower ; Infinity)Streamz offers a lot of intermediate computations, these are not effective until a blocking computation is requested :
Stream#map(mapper)
maps each element using the provided mapper functionStream#filter(predicate)
only gives back elements that satisfy the provided predicateStream#peek(functor)
applies the function on each element and passes itStream#unique()
removes duplicates (uses a Set
behind the scenes to check for duplicates)Stream#take(amount = 10)
only keeps the first x ≤ amount
elementsStream#skip(amount = 10)
skips the first x ≤ amount
elementsStream#between(begin = 0, end = 10, excludeRight = false)
takes the element whose index is in the range [begin ; end] if excludeRight == false
otherwise from [begin ; end)Stream#forEach(functor)
applies the provided function on each elementStream#reduce(reducer, acc)
computes the reduced value by getting a new acc
by applying the reducer function and the current acc
and the current elementStream#all(predicate)
checks whether or not every element satisfies the given predicateStream#any(predicate)
checks whether or not any element satisfies the given predicateStream#none(predicate)
checks whether or not none of the elements satisfies the given predicateStream#pack
is an instance of Packer
and exposes the following :
Packer#toArray()
converts the stream into an arrayPacker#toObjectBy(keyGen)
converts the stream into an object whose keys are generated using keyGen
on the current element and whose values are the elements of the streamPacker#toObject(keyGen, valueGen)
converts the stream into an object whose keys and values are generated using the current element on keyGen
and valueGen
Packer#toSet()
converts the stream into a Set
Packer#toMapBy(keyGen)
converts the stream into a Map
whose keys are generated using keyGen
on the current element and whose values are the elements of the streamPacker#toMap(keyGen, valueGen)
converts the stream into a Map
whose keys and values are generated using the current element on keyGen
and valueGen