Nu is small (~1k gzipped) Javascript library for creating and operating on lazy, potentially infinite streams. Streams are a simple ordered sequence abstraction, that are both lazy and persistent.
Knowledge, Pure Foundation
This is the empty stream
nu.NIL
cons
builds up a stream
Stream can be built by consing elements onto the front of an existing stream
nu.cons(1, nu.NIL);
This is a stream with 3 elements
var easy = nu.cons(1,
nu.cons(2,
nu.cons(3,
nu.NIL)))
Streams may contain any element type, including objects, functions, falsy values, and other streams.
nu.cons(
{x: 3},
nu.cons(
null,
nu.cons(
function(x) {},
nu.cons(
nu.cons(1, nu.NIL),
nu.NIL))))
first
reads the head of a stream
Streams do not provide random access.
nu.first(easy) // 1
rest
removes the head of a stream
Returning a new stream.
nu.first(nu.rest(easy)) // 2
nu.first(nu.rest(nu.rest(easy))) // 3
isEmpty
tests if a stream is empty
nu.isEmpty(easy) // false
nu.isEmpty(
nu.rest(
nu.rest(
nu.rest(easy)))) // true
Nu streams are persistent
Stream operations do not alter the original
var zeroIndexed = nu.cons(0, easy)
nu.first(easy) // 1
nu.first(zeroIndexed) // 0
Proving Through the Manifestation
from
creates a stream from an array
var abc = nu.from(['a', 'b', 'c']);
Or any array-like object, such as objects or strings.
var abc = nu.from('abc');
toArray
converts a stream an array
nu.toArray(abc) // ['a', 'b', 'c']
append
joins streams together
var easyThings = nu.append(
abc,
nu.from(['do', 're', 'mi']),
easy);
nu.first(easyThings) // a
nu.first(nu.rest(
nu.rest(nu.rest(easyThings)))) // 'do'
Nu Function in Conjunction
stream
defines a raw stream
It takes a first element, and a function (a thunk) that returns the rest of the stream.
var justOne = nu.stream(
1,
function() { return nu.NIL; })
Streams may be infinite
The function passed to stream
is only called when needed
var allOnes = nu.stream(
1,
function() { return allOnes; });
nu.first(allOnes) // 1
nu.first(nu.rest(allOnes)) // 1
nu.first(nu.rest(nu.rest(nu.rest(allOnes)))) // 1
var ht = allOnes;
for (var i = 0; i < 100000; ++i)
ht = nu.rest(ht)
nu.first(ht) // 1
Closures can capture state
var count = function(x) {
return nu.stream(
x,
function() { return count(x + 1); });
};
var counter = count(0)
nu.first(counter) // 0
nu.first(nu.rest(counter)) // 1
nu.first(nu.rest(nu.rest(counter))) // 2
nu.first(nu.rest(nu.rest(nu.rest(counter)))) // 3
nu.first(nu.rest(nu.rest(nu.rest(nu.rest(counter))))) // 4
A Fibonacci number stream
var fib = function(n1, n2) {
return function() { // returns the rest of the stream
return nu.stream(n1, fib(n2, n1 + n2));
};
};
var fibStream = nu.stream(0, fib(1, 1));
nu.first(fibStream) // 0
nu.first(nu.rest(fibStream)) // 1
nu.first(nu.rest(nu.rest(fibStream))) // 1
nu.first(nu.rest(nu.rest(nu.rest(fibStream)))) // 3
nu.first(nu.rest(nu.rest(nu.rest(nu.rest(fibStream))))) // 5
But some operations don't support infinite streams
You can't convert an infinite stream to an array.
nu.toArray(fibStream) // STALL
Making Use of the Knowledge That We Already Had
map
rewrites each element of a stream
var squares = nu.map(
function(x) { return x * x},
stream.from([1, 2, 3]))
nu.toArray(squares) // [1, 4, 9]
Transforms like map
are lazy, return in constant time, and work on infinite streams
var fibSquares = nu.map(
function(x) { return x * x},
fibStream);
nu.first(fibSquares) // 0
nu.first(nu.rest(fibSquares)) // 1
nu.first(nu.rest(nu.rest(nu.rest(nu.rest(fibSquares))))) // 25
Operation argument order encourages binding
var sqr = nu.map.bind(null,
function(x) { return x * x});
var squares = sqr(nu.from([1, 2, 3])))
nu.toArray(squares) // [1, 4, 9]
filter
selects elements from a stream
var odds = nu.filter(
function(x) { return x % 2; },
nu.from([1, 2, 3, 4, 5, 6]));
nu.toArray(odds) // [1, 3, 5]
zip
combines two streams into a stream of pairs
Taking as many elements as the shorter of the two streams contains.
var z = nu.zip(
nu.from('abc'),
nu.from([1, 2, 3, 4, 5, 6]));
nu.toArray(z) // [['a', 1], ['b', 2], ['c', 3]];
zipWith
is a generic zip
It combines elements with a custom binary function.
var z = nu.zipWith(
function(x, y) { return x + y },
nu.from('abc'),
nu.from([1, 2, 3, 4, 5, 6]));
nu.toArray(z) // ['a1', 'b2', 'c3'];
Just Be Real
forEach
iterates over a stream
nu.forEach(
function(x) {
console.log(x);
},
nu.from([1, 2, 3]));
But forEach
stalls on infinite streams
nu.forEach(console.log, fibStream) // STALL
foldl
maps and accumulates over a stream
nu.foldl(
function(accumulated, current) {
return accumulated + current;
},
0, // initial value
stream.from([1, 2, 3]))
// 6
foldl
folds left-to-right
nu.foldl(
function(a, c) { return [a, c]; },
0,
stream.from([1, 2, 3]))
// [[[0, 1] 2], 3]
foldr
folds right-to-left
nu.foldr(
function(a, c) { return [a, c]; },
0,
stream.from([1, 2, 3]))
// [0, [1, [2, 3]]]
Collaboration in a Style That's like Funk
Non-core functionality is included in small separate packages.
gen::repeat
repeats an element
nu.toArray(
gen.repeat(4, 'a'));
// ['a', 'a', 'a', 'a']
The count may be infinite
gen.repeat(Infinity, 'a'));
gen::range
generates a counted range
It's a bit like Python's range
.
An empty range is the infinite stream counting from 0
var s1 = nu.toArray(gen.range());
nu.first(s1) // 0
nu.first(nu.rest(s1)) // 1
Use an (exclusive) upper bound
nu.toArray(gen.range(4))
// [0, 1, 2, 3]
Generate a range [lower, upper)
nu.toArray(gen.range(2, 6))
// [2, 3, 4, 5]
nu.toArray(gen.range(2, -1))
// []
Use a custom step size.
nu.toArray(gen.range(2, 9, 3))
// [2, 5, 8]
Negative counting.
nu.toArray(gen.range(2, -4, -2))
// [2, 0, -2]
We're Here to Teach So You'd Might As Well Learn
Quantifiers test all stream elements. They fail early, but may stall on infinite streams
every
tests if a predicate is satisfied for all elements in a stream
quantifier.every(
function(x) { return x < 10; },
gen.range(4));
// true
every
works on infinite streams.
quantifier.every(
function(x) { return x < 10; },
gen.range());
// false
Unless the predicate is always satisfied
quantifier.every(
function(x) { return x >= 0; },
gen.range());
// STALL
any
tests if a predicate is satisfied for any element in a stream
quantifier.any(
function(x) { return x > 10; },
gen.range(4));
// true
any
works on infinite streams too.
quantifier.any(
function(x) { return x > 10; },
gen.range());
// true
Unless the predicate is never satisfied.
quantifier.any(
function(x) { return x < 0; },
gen.range());
// STALL
Stand Tall, or Don't Stand At All
The select
package allows taking and removing sub sections of streams.
select::take()
trims a stream
The new stream includes at most a set count number of elements.
var s = select.take(
6,
nu.map(
function(x) { return x * x; },
gen.range()));
nu.toArray(s) // [0, 1, 4, 9, 16, 25]
select::takeWhile
trims using a predicate
Instead of a count, it takes elements while the predicate is satisfied
var s = select.takeWhile(
function(x) { return x < 20 },
nu.map(
function(x) { return x * x; },
gen.range()));
nu.toArray(s) // [0, 1, 4, 9, 16]
select::skip
removes elements from a stream
It discards the first set count number of elements
var s = select.skip(
6,
nu.map(
function(x) { return x * x; },
gen.range()));
nu.first(s) // 6
select::skipWhile
does the same with a predicate
var s = select.skipWhile(
function(x) { return x < 20 },
nu.map(
function(x) { return x * x; },
gen.range()));
nu.first(s) // 20
Knowledge For What You Yearn
This site only provides a brief introduction to Nu. Examples of additional
functionality such as concat
, memoizing streams, and reduce
can be found in
the documentation.