peoro / scontainers

2 stars 0 forks source link

scontainers npm (scoped) NpmLicense David Travis (.com) Coveralls github

Scontainers is a container/collection/iterator library for JavaScript, comfortable to use, performant and versatile.

Scontainers offers functional-style operations like forEach, map, filter, reduce etc, similarly to lodash and underscore, but it...

Quick example

This example uses the straits syntax. Refer to the straits documentation to see how to use it with free functions or member symbols.

The following example transforms a string into the binary representation of its ASCII characters, then back to a string:

import scontainers from 'scontainers';

use traits * from scontainers; // enabling the .* operator

const encoded =
  "★Hey!★"                           // "★Hey!★"
  .*map( char=>char.charCodeAt(0) )  // 9733, 72, 101, 121, 33, 9733
  .*filter( num=>num<128 )           // 72, 101, 121, 33
  .*map( num=>num.toString(2) )      // "1001000", "1100101", "1111001", "100001"
  .*map( str=>str.padStart(8, '0') ) // "01001000", "01100101", "01111001", "00100001"
  .*join( ' ' );                     // "01001000 01100101 01111001 00100001"

const decoded =
  encoded.split(' ')                     // "01001000", "01100101", "01111001", "00100001"
  .*map( bin=>parseInt(bin, 2) )         // 72, 101, 121, 33
  .*map( num=>String.fromCharCode(num) ) // "H", "e", "y", "!"
  .*join();                              // "Hey!"

How it works

Scontainers introduces a set of traits (i.e. symbols to be used as property keys) used to express the capabilities relevant to collections and containers:

For instance, if an object has the symbol nth, then we know that such object is a collection whose elements can be enumerated, and the nth element of the collection can be accessed as object.nth(n).

Scontainers currently supports collections that can be accessed the following ways:

  1. using a key, that can be of any kind. An example of this is Map.
  2. using an index: an integer between 0 and the size of the collection. Unless a different key access function is provided, the index can also be used as a key to access elements in the collection using the previous mechanism.

Elements of collections are represented as a KVN object, rather than a key-value pair.

Core and derived traits

Scontainers defines a set of "core traits" that define some core properties of containers and collections and ways to access to these: the size of the collection, a way to access the nth element, the way to access an element with a given key, a way to tell whether an element belongs to the collection, ways to modify the collection, ways to iterate through the collection's elements etc. These core traits should be implemented only for those types or objects that can implement them natively and efficiently: the len trait to compute the size of a collection shouldn't be implemented by traversing the whole collection.

Other traits can be derived from existing ones, and these are called "derived traits". For instance, if all the elements of a collection can be accessed with indices ranging from 0 to len(), then the forEach and reverse operations can be automatically derived; if the len is available, or if it's possible to iterate through the whole collection, then count can be derived. These traits can of course be explicitly implemented as well, in case a smarter or more efficient implementation is available.

Iterators

Scontainers implements the Symbol.iterator trait compatible to the iterable protocol, but it also defines other iterators: kvIterator and kvReorderedIterator.

kvIterator is used the same way as iterator, but the objects next() returns is either a KVN or undefined.

kvReorderedIterator is able to handle synchronous iteration on reordered collections, like [3,1,2].*sort().

Transformations

Some of the traits defined by scontainers can be used to derive new collections from existing ones.

For instance map: it operates on a collection and returns a new collection with the same size; for each element <index,key,value> in the original collection, the resulting one has an element <index,key,f(value,key,index)>.

Another example is filter: it returns a collection like the original one stripped of the elements for which f(value,key,index) returns false.

Unless otherwise specified, transformations are lazy: they return a wrapper around the original collection, without processing or allocating any new data.

Examples of some types and their traits

Let's look at some examples of some types and the traits they can implement...

Code generation and trait generators

In order to massively speed up scontainers operations, the code for some traits can be generated at runtime.

Take the following example:

const c = [1,2,3];
const rc = c.*reverse();
const mrc = rc.*map( fn );
console.log( mrc.*nth(n) );

mrc.*nth(n) would call rc.*nth(n) which would call c.*nth(m) (with m=c.*len()-n) which would return c[m]. These nested calls are quite slow with the existing JavaScript engines. If you were to manually implement mrc.*nth(n), the efficient version you would write would simply be c[c.*len()-n].

Of course it's not possible to manually implement every operation for every collection and all their combination of transformations (which are infinite). What scontainers do is using "trait generators": some traits that expose all the necessary information to dynamically generate efficient code.

The code generation efforts are still ongoing: the generated code can be further optimized and code is not yet generated for many traits, but for many common operations scontainers is already achieving astonishing results.

API

Types

Range

Scontainers introduces Range: a virtual collection made of all the integers in a certain range.

new Range( [[begin,] end] )

Range has the members properties begin and end and the method len().

new Range();        // Range from 0 to Infinity
new Range(10);      // Range from 0 to 10
new Range(20, 55);  // Range from 20 to 55

const assert = require('assert');
const rng = new Range();
assert.eq( rng.begin, 0 );
assert.eq( rng.end, Infinity );
assert.eq( rng.len(), Infinity );

KVN

A KVN is an object used to describe an element of a collection.

A KVN has up to three fields:

This concept is similar to a key-value pair, but is also able to carry information on the element's enumerable order.

Let's see a couple of examples: the KVN of the only element of new Map([[2,"hey"]]) is {key:2, value:"hey"}. The KVN of the only element of ["yo"] is {key:0, n:0, value:"yo"}. The KVN of the only element of ["a", "b"].*slice(1) is {key:1, n:0, value:"b"}.

Most transformations try to preserve the original key of an element, while the index, if available, is always between 0 and .*len()-1.

Scontainers' native iterators (see the kvIterator and kvReorderedIterator traits) as well as functions meant to search for elements (e.g. first, random) return a KVN if an element is found or undefined if no element was found or if the end of the iterator has been reached.

Standard Types

The scontainers traits are implemented for the following types:

Object doesn't implement the scontainers traits directly, but the traits are implemented for the own properties and the enumerable properties of an object:

Operations

Scontainers defines the following traits:

The following operations must be asymptotically fast; preferably O(1).

Consumers:

Decorators:

Status of scontainers

Scontainers is still in its alpha stage. Some of its traits already work, but some might be broken and a few extremely important features are still missing.

In particular we still need to...