0.4.0 • Published 4 months ago

tinylazyseq v0.4.0

Weekly downloads
-
License
MIT
Repository
github
Last release
4 months ago

TinyLazySeq

Statements Branches Functions Lines * * Currently only Sequence and Utils classes count with testing. AsyncSequence testing is a bit trickier, so it will take longer.

Small ES6 library that provides generator-based lazy sequences, allowing functional intermediate operation composition computed on demand. For more information, here is the documentation.

Tiny note of warning:

Although I couldn't find any errors before publishing this project, I have not intensely tested this library. As such, there is a slim chance you run into a bug. If so, please let me know and I'll publish a patch as soon as I can. This warning will stay here until I feel the library is perfectly safe or I finally make a test suite for it.

Getting Started

To add TinyLazySeq to your project, just run the following command in your project folder:

npm install tinylazyseq

New in This Version

An instance method equivalent to the new Map.groupBy method in the stage 4 array grouping proposal has been added:

const seq = Sequence.of(
    { name: "María", grade: 5.0 }, 
    { name: "Juan" , grade: 6.5 }, 
    { name: "Pedro", grade: 3.7 },
    { name: "María", grade: 7.0 });

seq.groupBy(item => item.name);
/* ^ Map {
         "María" => [ { "name": "María", "grade": 7.0 }, { "name": "María", "grade": 5.0 }],
         "Juan"  => [ { "name": "Juan" , "grade": 6.5 } ],
         "Pedro" => [ { "name": "Pedro", "grade": 3.7 } ]
     }
*/

However, no equivalent to the Object.groupBy method in the same proposal made it to the project. I consider using Objects as Maps very bad practice: frequent dynamic addition of keys degrades property access performance heavily, so one should try to mutate anonymous objects as little as possible. As such, I decided against adding the method.

Laziness

The key difference between sequences and other iterables is their laziness - because of their lazy nature, sequences will do the minimal amount of work necessary to produce results, computing them on demand instead of eagerly producing the entire output.\ The drawback to this is that, overall, sequences are less performant than an eager collection when consuming the entire input. The advantage, however, is that lazy sequences won't halt the result production while all values get computed. To better explain what I mean, here's an example in pseudocode:

var range = inclusiveRange(1, 10)

var list = range.toList()
var seq  = range.toSequence()

list.filter(num -> num % 2 == 0).map(num -> num * num)
seq.filter(num -> num % 2 == 0).map(num -> num * num)

These seemingly identical operations do, broadly, the same thing: they filter a collection of numbers from 1 to 10, keeping only even numbers, and then multiply those numbers by themselves. If we were to exhaust both collections with a forEach, we'd get the same output:

list.forEach(print) // 4, 16, 36, 64, 100
seq.forEach(print)  // 4, 16, 36, 64, 100

However, if we add a log to each operation, we can clearly see the different nature of both approaches:

list.filter(num -> { 
    print("filtering")
    return num % 2 == 0 
}).map(num -> {
    print("mapping")
    return num * num
})
/** output:
 * filtering, filtering, filtering, filtering, filtering
 * filtering, filtering, filtering, filtering, filtering
 * mapping, mapping, mapping, mapping, mapping
 */

seq.filter(num -> { 
    print("filtering")
    return num % 2 == 0 
}).map(num -> {
    print("mapping")
    return num * num
})
/** output:
 * none
 */

list.forEach(print) 
// 4, 16, 36, 64, 100

seq.forEach(print)
/** output:
 * filtering, filtering, mapping, 4
 * filtering, filtering, mapping, 16
 * filtering, filtering, mapping, 36 
 * filtering, filtering, mapping, 64
 * filtering, filtering, mapping, 100
 */

At this point, I hope I have done a good enough job of explaining the power of lazy sequences and intermediate operations. The List approach had all values immediately available after 15 operations, while the Sequence approach had the first value as soon as 3 operations.

Factories

There are multiple ways of defining a sequence, but the two most commonly used are:

Async/Sequence.of<T>(...args: T[]): Sequence<T>

Creates a lazy sequence containing the provided arguments.

import { Sequence } from "tinylazyseq";
Sequence.of(1, 2, 3, 4, 5);

or an asynchronous one instead:

import { AsyncSequence } from "tinylazyseq";
AsyncSequence.of(promiseTask1(), promiseTask2(), promiseTask3());

Async/Sequence.from<T>(iterable: Iterable<T>): Sequence<T>

Creates a lazy sequence wrapping the provided iterable.

import { Sequence } from "tinylazyseq";
Sequence.from(getSomeIterableData());

or an asynchronous one instead

import { AsyncSequence } from "tinylazyseq";
AsyncSequence.from(getSomePromiseArray());

Since the rest of Sequence factories are as straightforward as these, I think the inline docs do a good enough job of explaining how they work.

Single Iteration Constraints

TinyLazySeq supports Sequences made from iterators, as opposed to iterables, which can only be consumed once:

const iterator = someCustomIterator();

// this Sequence can only be iterated once
const seq = Sequence.from(iterator);

// we exhaust the Sequence through the forEach terminal operation
seq.map(someTransform).filter(somePredicate).forEach(console.log);

// calling another terminal operation results in an IllegalStateError
seq.fold(initial, reducer); 
//  ^ IllegalStateError: attempted to iterate a constrained sequence more than once

Iterable derived Sequences can also be constrained to one iteration, in Kotlin Sequence fashion:

const seq = Sequence.from([1, 2, 3, 4, 5]).constrainOnce();

// first() is terminal
console.log("first item!", seq.first());

// error, already consumed
seq.forEach(consumer);
//  ^ IllegalStateError: attempted to iterate a constrained sequence more than once

API

A full description of all methods can be found here.

The Sequence API is very similar to the Array API, so if you know how to use a functional approach with a JavaScript array, you pretty much already know how to use a Sequence. Here's a comparison table between Array and Sequence:

Method or propertyArraySequence
lengthyesno*
fromyesyes
ofyesyes
atyesno, but elementAt
concatyesyes
containsno, but includesyes
containsAllnoyes
copyWithinyesno, immutable
countnoyes
dropno, but sliceyes
dropWhilenoyes
elementAtno, but atyes
entriesyesno
everyyesyes
fillyesno, immutable
filteryesyes
findyesyes
findIndexyesyes
findLastnoyes
findLastIndexnoyes
firstnoyes
flat / flattenyesyes
flatMapyesyes
foldno, but reduceyes
forEachyesyes
includesyesno, but contains
indexOfyesyes
isEmptynoyes
joinyesyes
lastnoyes
lastIndexOfyesyes
mapyesyes
popyesno, immutable
pushyesno, immutable
reduceyesyes
reduceRightyesno, can't be iterated backwards
reverseyesno, immutable
shiftyesno, immutable
sizeno, but lengthyes, partially
sliceyesno, but drop and take
someyesyes
sortyesno, immutable
spliceyesno, immutable
takeno, but sliceyes
takeWhilenoyes
toLocaleStringyesno
toStringyesyes, but does not provide the values
unshiftyesno, immutable
valuesyesno
Map.groupByyesboth: map static and instance method
Object.groupByyesonly object static (discouraged)

* since Sequences describe possibly unsized and/or infinite collections, it is impossible to have a length property. Instead, sequences try to infer the size of the underlying collection from their available information (eg. the collection implements size or length), providing the size if they do so succesfully, or an integer smaller than zero if the size is unknown.

Contact

I'm easily contactable through Discord as maruseron. Not really active anywhere else.

0.4.0

4 months ago

0.3.0

2 years ago

0.2.1

2 years ago

0.2.2

2 years ago

0.1.4

2 years ago

0.1.3

2 years ago

0.1.2

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago