Awesome
Underarm
Use JavaScript transducers with the familiar Underscore.js API with extra goodies like lazy generators and callback processes.
If you are not familiar with transducers, check out Transducers Explained.
Too much API for you? Just grab what you need from the transduce libraries, which underarm is based.
Install
Works with any-promise library (a pollyfill, es6-promise, promise, native-promise-only, bluebird, rsvp, when, q ... your choice) for asynchronous execution. Underarm allows any transducer to become asynchronous: Promises can be used and returned in init
, step
and result
.
Install your Promise library preference before underarm and it will be auto detected and used.
$ npm install promise # or es6-promise, bluebird, q, when, rsvp ... see any-promise
$ npm install underarm
$ bower install underarm
Browser
Include an ES6 Promise Pollyfill. Then include the browser version of underarm.
Structured to allow creation of custom builds by loading only desired libs. For example, see:
Created by using browserify
with this loader.
Transducers
First some helper functions and imports.
// import, mixin and helper functions
var _r = require('underarm');
function isEven(x){
return x % 2 !== 1;
}
function inc(x){
return x+1;
}
// prints every result and input. Useful with tap
function printIt(result, input){
console.log(input+' ['+result+']');
}
var trans, result;
Chaining transducers is the same as function composition. Composed transducers are executed left to right.
result = _r.into([], _r.compose(_r.filter(isEven), _r.map(inc)), [1,2,3,4]);
// [ 3, 5 ]
// these are also the same
trans = _r().filter(isEven).map(inc).compose();
result = _r.into([], trans, [1,2,3,4,5]);
result = _r().filter(isEven).map(inc).toArray([1,2,3,4,5]);
// [ 3, 5 ]
Like underscore, use tap
to intercept intermediate results. Accepts current result and item just like the step function. The return value is ignored.
result = _r()
.filter(function(num) { return num % 2 == 0; })
.tap(printIt)
.map(function(num) { return num * num })
.toArray([1,2,3,200]);
// 2 []
// 200 [4]
// [4, 40000 ]
Support for underscore collection functions.
result = _r().invoke('sort').toArray([[5, 1, 7], [3, 2, 1]]);
// [ [ 1, 5, 7 ], [ 1, 2, 3 ] ]
var stooges = [{name: 'moe', age: 40}, {name: 'larry', age: 50}, {name: 'curly', age: 40}];
result = _r.into([], _r.pluck('name'), stooges);
// ['moe', 'larry', 'curly' ]
result = _r.into([], _r.where({age: 40}), stooges);
// [ { name: 'moe', age: 40 }, { name: 'curly', age: 40 } ]
result = _r.into([], _r.findWhere({age: 40}), stooges);
// [ { name: 'moe', age: 40 } ]
result = _r.into([], _r.every(isEven), [0, 2, 8, 4, 8]);
// [true]
result = _r.into([], _r.some(isEven), [1, 3, 7, 11, 9]);
// [false]
result = _r.into([], _r.contains(3), [1, 3, 7, 11, 9]);
// [true]
result = _r.into([], _r.find(isEven), [7, 8, 7, 11, 12]);
// [8]
result = _r.into([], _r.first(3), [1, 9, 13, 11, 9]);
// [1, 9, 13 ]
Transducers implemented
Base
Base Transducers mixing common functionality from supported transduce implementations.
map(f)
Step all items after applying a mapping function to each item. Wraps mapping function with _r.iteratee
.
Alias: collect
filter(predicate)
Step all the items that pass a truth test. Wraps predicate with _r.iteratee
.
Alias: select.
remove(predicate)
Step all the items that fail a truth test. Wraps predicate with _r.iteratee
.
Alias: reject
take(n?)
Step the first item if n
is undefined. Passing n
will step the first N values in the array.
Resolves as single value if n is undefined.
Alias: first, head
takeWhile(predicate)
Takes items until predicate returns false. Wraps predicate with _r.iteratee
.
drop(n?)
Steps everything but the first item if n is undefined. Passing an n
will drop N values.
Alias: rest, tail
dropWhile(predicate)
Drops items while predicate returns true. Wraps predicate with _r.iteratee
.
cat
Concatenating transducer.
NOTE: unlike libraries, cat should be called as a function. Use _r.cat()
instead of _r.cat
mapcat(f)
Composition of _r.map(f)
and _r.cat()
. Wraps mapping function with _r.iteratee
partitionAll(n)
Partitions the source into arrays of size n. When transformer completes, the array will be stepped with any remaining items.
Alias: chunkAll
partitionBy(f)
Partitions the source into sub arrays while the value of the function changes equality. Wrap partitioning function by _r.iteratee
.
compact()
Trim out all falsey values.
invoke(method)
Invoke a method (with arguments) on every item.
pluck(key)
Convenience version of a common use case of map
: fetching a property.
where(attrs)
Convenience version of a common use case of filter
: selecting only objects containing specific key:value
pairs.
Array
forEach(iteratee)
Passes every item through unchanged, but after executing callback(item, idx)
. Can be useful for "tapping into" composed transducer pipelines. The return value of the callback is ignored, item is passed unchanged. (See transduce-stream for a use case.)
Alias: each
find(predicate)
Like filter, but terminates transducer pipeline with the result of the first item that passes the predicate test. Will always step either 0 (if not found) or 1 (if found) values.
Resolves as single value.
Alias: detect
findWhere(attrs)
Convenience version of a common use case of find
: getting the first object containing specific key:value
pairs. Early termination when found.
Resolves as single value.
every(predicate?)
Checks to see if every item passes the predicate test. Steps a single item true
or false
. Early termination on false
. Wraps predicate in _r.iteratee
Resolves as single value.
Alias: all
some(predicate?)
Checks to see if some item passes the predicate test. Steps a single item true
or false
. Early termination on true
. Wraps predicate in _r.iteratee
Resolves as single value.
Alias: any
contains(target)
Does the stream contain the target value (target === item
)? Steps a single item true
or false
. Early termination on true
.
Resolves as single value.
Alias: include
push(...args)
Passes all items straight through until the result is requested. Once completed, steps every argument through the pipeline, before returning the result. This effectively pushes values on the end of the stream.
unshift(...args)
Before stepping the first item, steps all arguments through the pipeline, then passes every item through unchanged. This effectively unshifts values onto the beginning of the stream.
at(index)
Retrieves the value at the given index. Similar to indexing into an array.
Resolves as single value.
slice(begin? end?)
Like array slice, but with transducers. Steps items between begin
(inclusive) and end
(exclusive). If either index is negative, indexes from end of transformation. If end
is undefined, steps until result of transformation. If begin
is undefined, begins at 0.
Note that if either index is negative, items will be buffered until completion.
initial(n?)
Steps everything but the last entry. Passing n
will step all values excluding the last N.
Note that no items will be sent and all items will be buffered until completion.
last(n?)
Step the last element. Passing n
will step the last N values.
Resolves as single value if n
is undefined
Note that no items will be sent until completion.
unique(isSorted?, iteratee?)
Produce a duplicate-free version of the transformation. If the transformation has already been sorted, you have the option of using an algorithm that maintains less state. If iteratee is passed, it will be wrapped with _r.iteratee
and use return value for comparison.
Alias: uniq
Math
min(f?)
Steps the max value on the result of the transformation. if f
is provided, it is wrapped with _r.iteratee
and called with each item and the return value is used to compare values. Otherwise, the items are compared as numbers.
Resolves as single value.
max(f?)
Steps the max value on the result of the transformation. if f
is provided, it is wrapped with _r.iteratee
and called with each item and the return value is used to compare values. Otherwise, the items are compared as numbers.
Resolves as single value.
Strings
Strings are a sequence of characters, so you can transduce over those as well. Particularly useful with transduce-stream.
Functions that split
over streams are treated as a substring, and splits across the entire transformation. This allows methods to work with chunks sent through streams. Methods that split
over the String are processed lazily and as soon as possible: lines
, words
and chars
will process a line/word/char as they are received, and buffer any intermediate chunks appropriately.
split(separator, limit)
Works like ''.split
but splits across entire sequence of items. Accepts separator (String or RegExp) and limit of substrings to send.
join(separator)
Buffers all items and joins results on transducer result
.
Resolves as a single value.
nonEmpty()
Only steps items that are non empty strings (input.trim().length > 0
).
lines(limit?)
Split chunks into lines and steps each line up to the optional limit.
chars(limit?)
Split chunks into characters and steps each char up to the optional limit.
words(delimiter?, limit?)
Split chunks into words and steps each word up to the optional limit. Can pass an optional delimiter to identify words, default on whitespace (/\s+/
).
Chaining
Chaining is implicit when calling as a function _r()
, and the source can be optionally passed as an argument to the function. When chaining, each of the transducers can be called as a builder similar to underscore.
value()
Call on a chained transformation to sequence a wrapped value through the transformation and resolve as a value. The value returned is similar to underscore. Transducers that resolve as a single value are noted above. If a function does not resolve as a single value, resolves a a collection determined by _r.empty(source)
(see below, normally an array).
compose()
Call on chained transformation to compose all transducer functions in chain and return a transducer that can be used to pass to transduce functions.
Alias: transducer
mixin()
Mixin custom transducer creating functions. Transducer creating functions will be called with arguments passed when chaining and should return a transducer. All transducers described above are mixed in.
Transduce functions
Adds functionality from transduce with default values from dispatched functions and chained transformations.
toArray(xf?, from?)
Returns a new orray by iterating throughfrom
after running it through the optional transformation.
If called on a chained transformation, uses the composed transformation as xf
. If from
is not defined, uses the wrapped source when creating the chain, _r(wrapped)
.
into(to?, xf?, from?)
Returns a new collection appending all items into the empty collection to
by passing all items from source collection from
through the transformation xf
.
Uses generic dispatch _r.append
for reducing function. If the from
collection is undefined, creates an empty collection using _r.empty()
. If xf
is a chained transformation, composes it.
Delegates to _r.transduce(xf, _r.append, to, from)
if xf is defined, otherwise
_r.reduce(_r.append, to, from)if
xf` is undefined.
If called on a chained transformation, uses the composed transformation as xf
. If from
is not defined, uses the wrapped source when creating the chain, _r(wrapped)
. If to
is not defined, uses _r.empty(from)
to create an empty value.
transduce(xf?, f, init, coll?)
Transduce over a transformation using the installed transducers library, using chained transformation if chaining. If chaining and the source collection is not defined, uses wrapped source passed when creating the chain _r(wrapped)
. Calls generic dispatch _r.unwrap
with result before returning.
reduce(f, init, coll?)
Reduces over a transformation using the installed transducers library. If the source coll
is undefined, or null, creates an empty source by dispatching to _r.empty(coll)
.
Alias: foldl, inject
reduced
Ensures values are reduced to signal early termination when stepping through a transformation. Probably only useful if you want to mixin custom transducers. Use _r.isReduced
to check if a value is wrapped as reduced
. Use generic dispatch _r.unwrap
to unwrap reduced values.
Async
Uses transduce-async to support promises in transducer init
, step
and result
.
prototype.async()
Marks chained transformation as asynchronous. See below for changes to API when async
.
prototype.value()
If chained transformation is async
returns a promise for the value of the transformation
prototype.then(resolve, reject)
Marks chained transformation as async
and adds Promise listeners to Promise value
. This means that any chained transformation is a promise.
compose(*fns)
Like a normal compose when chained transformation not async
. If async
all arguments are interleaved with defer
. This allows any transducer in composed pipeline to step
or result
a Promise in addition to a value. The wrapped transformer is called with value of resolved Promise.
transduce(xf?, f, init, coll?)
Like a normal transduce when chained transformation is async
. If async
, init
and coll
can be a Promise and xf
can be an async transducer. The value of coll
can be anything that can be converted to an iterator using transduce. The return value is a Promise for the result of the transformation.
into(to?, xf?, from?)
Like a normal into
when chained transformation not async
. If async
, to
and from
can be a Promise and xf
can be an async transducer.
toArray(xf?, from?)
Like a normal toArray
when chained transformation not async
. If async
, coll
can be a Promise and xf
can be an async transducer.
defer()
Create an async transducer that allows wrapped transformer to step
or result
a Promise in addition to a value. All items will be queued and processed asap. The wrapped transformer is called with value of resolved Promise.
delay(wait)
Create an async transducer that delays step of wrapped transformer by wait
milliseconds. All items will be queued and delayed and step
will return a promise that will resolve after wait
milliseconds for each item.
Iterators
Transduce, into, etc. accept generators or any iterator in addition to arrays. An iterator can either adapt the protocol or simply define a next function.
function* genNums(){
yield 1;
yield 2;
yield 3;
yield 4;
}
result = _r.into([], _r.first(3), genNums());
// [1, 2, 3 ]
Sequence generation
Generate will create an iterator that executes a callback. This can be used to generate potentially infinite sequences.
// Returns a function that generates the next fibonacci number
// every time it is called. Use with generators below
function fib(){
var x=1, y=1;
return function(){
var prev = x;
x = y;
y += prev;
return prev;
}
}
If not chaining, creates an iterator that can be used with transduce. If chaining, creates an iterator a wraps the result. Pass true as second argument optionally call function to init on each iteration (allows reuse).
// call on init to allow reuse
trans = _r().generate(fib, true).first(7);
result = trans.value();
result = trans.value();
Callback Processes
Transducers can be consumed by reduce, but since they are designed to be independent, we can use them in a variety of contexts that consume an input and produce a result, such as CSP. We can also create a process using a callback where each call advances a step in the process. These can be used as event handlers (like the demo).
var $demo = $('#demo3'),
coords = _r()
.where({type:'mousemove'})
.map(function(e){return {x: e.clientX, y: e.clientY}})
.map(function(p){return '('+p.x+', '+p.y+')'})
.each(updateText)
.asCallback(),
click = _r()
.where({type:'click'})
.each(updateCount)
.asCallback(),
events = _r()
.each(coords)
.each(click)
.asCallback();
$demo.on('mousemove click', events);
function updateText(p){
$demo.html(p);
}
function updateCount(e, idx){
$demo.html('Click '+idx);
}
We are composing transducers. The previous examples are all using transducers behind the scenes. Method chaining is implicit and is composition, _r.generate
uses an iterator and passes on to transduce
. Even asCallback
uses transducers but steps through the results using the argument of a callback, instead of reducing over the results.
tap(interceptor)
Transduce also adds tap
, which invokes interceptor with each result and item, and then steps through unchanged. The primary purpose of this method is to "tap into" a method chain, in order to perform operations on intermediate results within the chain. Executes interceptor with current result and item.
Node Async
If you are using Node.js, asyncCallback
returns a callback that follows the standard convention of fn(err, item)
and accepts a continuation that is called on completion or error.
Streams
You can transduce over Node.js Streams with transduce-stream.
// test.js
var _r = require('underarm');
stream = require('transduce-stream');
var transducer = _r()
.words()
.map(function(x){return (+x * +x)+ ' '})
.uniq()
.take(4)
.push('\n')
.compose();
process.stdin.resume();
process.stdin.pipe(stream(transducer)).pipe(process.stdout);
Run this from the terminal to calculate a formatted sequence of the first 4 unique squared values.
$ echo '33 27 33 444' | node test.js
1089 729 197136
$ node test.js << EOT
12 32
33 33
33 43
12 33 12
EOT
144 1024 1089 1849
Generic dispatch
Since input and output are separated the transducer transformation, transducers can be reduced, and sequences can be created over any object that supports the following methods.
iterator(value)
Returns an iterator that has next function and returns {value, done}
. Default looks for object with iterator Symbol (or '@@iterator'
)
iteratee(value)
Just like underscore, you can use "where style" objects and strings with functions that expect a mapping function or a predicate. By default, uses _.iteratee
to match this behavior, but setup as a dispatched function to allow custom behavior.
empty(value?)
Returns empty object of the same type as argument. Default returns []
if _.isArray
or undefined
, {}
if _.isObject
and an internal sentinel to ignore otherwise (used when not buffering in asCallback
or chained value
expects single value.
append(result, item)
Accepts an item and optional key and appends the item to the object. By default, appends to arrays and objects by key and returns last item when used in asCallback
or chained transducer with single value
.
wrap(value?) / unwrap(value)
When chaining transducers, the object passed to _r(obj)
is dispatched to _r.wrap
. By default, the object is not wrapped if it is defined, and wrapped with _r.empty()
if not defined. When transducing over the sequence (with value
, into
, etc.) the object is then unwrapped with _r.unwrap
. By default, unwrap calls _r().value()
on chained transformations, extracts value from _r.reduced
or simply returns the value. You can provide custom dispatchers for custom wrapped values.
Example
You can dispatch to custom objects by registering supporting dispatch functions. Say, for example, you love using immutable collections.
var _r = require('underarm'),
_ = require('underscore'),
Immutable = require('immutable'),
Vector = Immutable.Vector;
_r.iterator.register(function(obj){
if(obj instanceof Vector){
return obj.values();
}
});
_r.empty.register(function(obj){
if(obj instanceof Vector){
return Vector.empty();
}
});
_r.append.register(function(obj, item){
if(obj instanceof Vector){
return obj.push(item);
}
});
function mult(x){
return function(y){
return x * y;
}
}
function isEven(x){
return !(x % 2);
}
var vector = Vector(1,2,3,4);
// Vector [ 1, 2, 3, 4 ]
_r.sequence(vector);
// Vector [ 3, 6, 9, 12 ]
_r.sequence(_r.map(mult(3)), vector);
// Vector [ 7, 14, 21, 28 ]
_r(vector).map(mult(7)).sequence();
// Vector [ 14, 28 ]
_r(vector).map(mult(7)).filter(isEven).sequence();
// Vector [ 1, 2, 3, 4 ]
_r(vector).sequence();
By default, value
transduces into an empty array if multiple values are expected, and a single value if single value is expected to match the Underscore API. To override the behavior of multiple values, simply register a dispatch an empty for an undefined object (value
calls _r.empty()
which by default returns an array).
// [ 1, 2, 3, 4 ]
_r(vector).value();
_r.empty.register(function(obj){
if(_.isUndefined(obj)){
return Vector.empty();
}
});
// Vector [ 1, 2, 3, 4 ]
_r(vector).value();
Utils
Finally, adds utils from transduce.
compose()
Simple function composition of arguments. Useful for composing (combining) transducers.
iterable(value)
Returns the iterable for the parameter. Returns value if conforms to iterable protocol. Returns undefined
if cannot return en iterable.
The return value will either conform to iterator protocol that can be invoked for iteration or will be undefined.
Supports anything that returns true for isIterable
and converts arrays to iterables over each indexed item. Converts to functions to infinite iterables that always call function on next
transformer(value)
Attempts to convert the parameter into a transformer. If cannot be converted, returns undefined
. If defined, the return value will have init
, step
, result
methods that can be used for transformation. Converts arrays (arrayPush
), strings (stringAppend
), objects (objectMerge
), functions (wrap as reducing function) or anything that isTransformer
into a transformer.
protocols
Symbols (or strings that act as symbols) for @@iterator
and [@@transformer
][10] that you can use to configure your custom objects.
identity(value)
Always returns value
arrayPush(arr, item)
Array.push as a reducing function. Calls push and returns array;
objectMerge(object, item)
Merges the item into the object. If item
is an array of length 2, uses first (0 index) as the key and the second (1 index) as the value. Otherwise iterates over own properties of items and merges values with same keys into the result object.
stringAppend(string, item)
Appends item onto result using +
.
is{Array, String, RegExp, Number, Undefined}
Predicates for object types
License
MIT