RaptorJuice
Preface
RaptorJuice originated as a utility library for Elixir. It implements iterators, type checking and other generally missed utilities in JavaScript. It is lightweight, (and may become lighter in the future) at around 6.5k uglified.
Purpose
The main goal for RaptorJuice is to implement iterators, or generator objects, with a concise and robust API.
Installation
- Ensure that you have Node.js on your system.
RaptorJuice is entirely self contained and published on npm so all you need to do is:
npm install raptorjuice
Development
-
Ensure that you have Node.js on your system.
-
Prepare your environment by executing the following commands:
git clone https://github.com/junglefresh/RaptorJuice.js raptorjuice cd raptorjuice npm install sudo npm install -g grunt-cli mocha
Testing
To automatically run tests upon saving changes to the source or tests run the following:
mocha --reporter spec ./spec/**/*.js ./spec/*.js
-
Upon execution of the following command, after each write:
- The unit tests will be confirmed.
Building
To begin automatically building the project, run:
grunt watch
-
Please note that a Java Runtime is required due to the jsdoc dependency.
-
By executing the following command, upon each write:
- The unit tests will be confirmed.
- Documentation will be regenerated if all tests pass.
Demonstration
If you are interested in any of RaptorJuice's secondary functionality, please refer to the documentation. As stated above, RaptorJuice's primary goal is to create generator objects, and more specifically, to greatly simplify asynchronous iteration.
Forward Iteration
An iterator
object's default direction is forward.
var gen = R.iterate([ 1, 2, 3 ]);
while(gen.remaining()) {
console.log(gen.next());
}
As you probably expect, this prints the values: 1
, 2
, and 3
.
Backward Iteration
Iterators
can also be reversed.
var gen = R.iterate([ 1, 2, 3 ]).reversed();
while(gen.remaining()) {
console.log(gen.next());
}
Note thate the reversed
method actually makes a new iterator.
This prints the values in the array in reversed order.
Previous Values and Reverse
An example of iterator reversal follows.
var gen = R.iterate([ 1, 2, 3, 4 ]);
gen.next(); // 1
gen.next(); // 2
gen.reverse().next(); // 2
gen.reverse({ discard : true }).next(); // 3
As a convenience, a prev
method also exists. It calls the reverse
method, then the next
method, followed by reverse
again. It takes
no parameters.
var gen = R.iterate([ 1, 2, 3, 4 ]);
gen.next(); // 1
gen.next(); // 2
gen.prev(); // 2
gen.next(); // 2
gen.next(); // 3
Remaining and Length Counts
The remaining
count is direction dependent.
var gen = R.iterate([ 1, 2, 3 ]);
gen.next();
gen.remaining(); // 2
gen.reverse();
gen.remaining(); // 1
gen.reverse();
gen.remaining(); // 2
This is in contrast to the length
method, which always returns the
actual length of the underlying data model.
var gen = R.iterate([ 1, 2, 3 ]);
gen.next();
gen.length(); // 3
gen.reverse();
gen.length(); // 3
gen.reverse();
gen.length(); // 3
The length
is modified automatically by the push
and pop
methods.
Copyable Iterators
Iterator
objects can be copied at any time. The copy will have the same
state as the original.
var gen = R.iterate([ 1, 2, 3 ]);
gen.next(); // 1
var cpy = gen.copy();
cpy.next(); // 2
gen.next(); // 2
Each copy will have it's own model, so modifications to one will not affect the other.
Push and Pop
The push
and pop
methods allow us to modify the modet itself are
indexed absolutely. Indexes are 1
based, as opposed to the standard
0
based indicies we are used to.
This was done for mathematical simplicity.
var gen = R.iterate([ 1, 2, 3 ]);
gen.push(1, [ 4 ]);
gen.data(); // [ 4, 1, 2, 3 ];
gen.next(); // 4
gen.pop(1); // [ 4 ]
Iterators
can be created empty, then populated later.
var gen = R.iterate();
gen.push([ 1 ]);
gen.push([ 2, 3 ]);
gen.data(); // [ 1, 2, 3 ]
The pop
method also may also be supplied with a count parameter to
specify how many elements to remove.
var gen = R.iterate([ 1, 2, 3, 4 ]);
gen.next(); // 1
gen.next(); // 2
gen.pop(1, 2); // [ 1, 2 ]
gen.prev(); // undefined
gen.next(); // 3
gen.prev(); // 3
Data and Index
The underlying model and cursor index are available through the data
and index
methods.
var gen = R.iterate([ 1, 2, 3, 4 ]);
gen.data(); // [ 1, 2, 3, 4 ]
gen.next(); // 1
gen.next(); // 2
gen.index(); // 2
Transform and Generate
To generate the Fibonacci Sequence we could do something like the following.
var fib = R.iterate([ 0, 1 ]);
fib.transform(function(v, u) {
this.push([ v + u(1) ]);
return v;
});
fib.next(); // 0
fib.next(); // 1
fib.next(); // 1
fib.next(); // 2
fib.next(); // 3
fib.next(); // 5, and so on...
This allows us to quite concisely calculate the set of Fibonacci numbers up to the first one over 10,000.
var fib = R.iterate([ 0, 1 ]).transform(function(v, u) {
this.push([ v + u(1) ]);
return v;
});
while(fib.cursor(true) < 10000) {
R.log(fib.next());
};
Passing a truthy value to the cursor
method ensures that it returns
a value, meaning it will be bumped off either end. This lets us initiate
a while loop more simply.
Our previous implementation isn't very robust. What would happen if we were to traverse our iterator in reverse? We would be appending values to the end of our iterator and this is not what we want.
var fib = R.iterate([ 0, 1 ]);
fib.transform(function(v, u) {
if( (this.direction() === 'forward') && !u(2) && u(1) ) {
this.push([ v + u(1) ]);
}
return v;
});
This second version of our Fibonacci generator works as expected. We generate values when traversing forward, and only when we need them.
fib.next(); // 0
fib.next(); // 1
fib.next(); // 1
fib.next(); // 2
fib.next(); // 3
There is only one catch. Since we generate our values with the transform
method, we always have more elements in the array than we actually need.
In most cases, this shouldn't be a problem, but what if this is
unacceptable for your implementation? You need the generate
method,
which is called when attempting to move off either end of the model. The
value returned by the generate
method will be added to the model before
actually moving allowing us to create our values on the fly.
var fib = R.iterate([ 0, 1 ]);
fib.generate(function(u) {
var n = u(-2) + u(-1);
if(!isNaN(n)) { return n; }
});
Now we have a Fibonacci sequence generator from zero to infinity. Not too
shabby, but it gets slightly better. As you'd probably expect, the
tranform
method will still be called on generated values.
fib.transform(function(v, u) {
return Math.pow(2, v);
});
We can now generate and caches the Fibbonacci sequence only when we need
to. When our generator returns a value from its next
or prev
functions, those values will be returned as powers of two.
fib.next(); // 1
fib.next(); // 2
fib.next(); // 2
fib.next(); // 4
fib.next(); // 8
fib.next(); // 32
fib.next(); // 256
At any point the generate
and transform
methods can be unset by
providing a falsy value to either method.
Chaining Iterators
We can acomplish some pretty interesting things by chaining iterators.
var a = R.iterate([ 1, 2, 3 ]);
var b = R.iterate([ 4, 5, 6 ]);
var c = R.iterate([ 7, 8, 9 ]);
var chain = R.chain(a, b, c);
while(chain.remaining()) {
R.log(chain.next());
});
Again, it's slightly better than just that. Chains actually call
their component iterator's next
method. This means their remaining
count is consistent even when being incremented as part of a chain.
Reversal of iterators that are part of a chain is not yet handled.
var a = R.iterate([ 1, 2, 3 ]);
var b = R.iterate([ 4, 5, 6 ]);
var c = R.iterate([ 7, 8, 9 ]);
var chain = R.chain(a, b, c);
chain.next(); // 1
chain.next(); // 2
a.remaining(); // 1
b.remaining(); // 3
chain.next(); // 3
chain.next(); // 4
a.remaining(); // 0
b.remaining(); // 2
Conclusion
By using RaptorJuice's iteration techniques, we can flatten, and iterate over a presently indeterminate sequence of values.
When all is said and done, RaptorJuice implements more of a dequeue than an iterator, strictly speaking.