le accidental occurrence

42at

_xiterator: an expression iterator for Underscore

without comments

Underscore.js is an excellent, compact Javascript  library extending the language with useful tools.  Most of the utils are drawn from Prototype and/or been inspired by languages such as Ruby. Underscore reverts to native code where it is supported.

A hand-full of so-called Collection functions which operate on arrays & objects, such as _any(), _all(), _map(), and _detect(), take an iterator function as an argument:

if ( _.any([1,2,3], function iterator(value){
    return ! _.isNumber(value);
})) {
    // ...
}

_.isNumber() is one of a dozen or so type checking routines provided by Underscore, in addition to, _isString(), _isFunction(), _isArray(), and others.

Often times the iterator is a simple type-checking, like above, or some sort of expression that needs to be evaluated.  So why not just enter the expression:

if (_.any([1,2,3], "!isNumber")) {
 // ...
}

This is what _xiterator, an add-on for Underscore, provides.  It is simply the meat of the iterator in a compact semantic.

Let’s throw in some range checking:

if (_.any([1,2,3], function iterator(value){
    return ! _.isNumber(value) || value < 0 || value > 10;
})){
    // ...
}

can be replaced by:

if (_.any([1,2,3], "!isNumber || < 0 || > 10")){
    // ....
}

Expression iterators

The expression is composed of valid Javascript code.  In the case of relational operators (<, <=, ==, etc.) the left operand is implied to be the value of the iterator.  The expression is evaluated in global scope.

Any of the Underscore _.isXXX type-checking routines can be used without the argument (the _.  part is implied).  In addition, three new routines are added: isBlank, isOdd, isEven.

Parenthetical expressions are fully supported:

 _.any([1,2,3], "(isNumber && > 0) || (isString && !isBlank)");

Validating functions:

 function checkZip(value) {...}

 _.any(zips, "!isBlank && checkZip(__value) ");  // __value is the placeholder

In addition to __value, __key and __list placeholder variables, corresponding to the formal arguments of the iterator, are available.

How about a regular expression:

 _.all([1,2,3], /\d+/);   // either as string or RegExp object

Of course, functions are still supported and will work as before.

Accessing original methods

Since the expression string needs to be parsed, runtime performance will be affected.  Performance sensitive applications involving huge sets should use the original routine.

The original routines  can be explicitly accessed by passing no argument, eg.

var originalAny = _.any();
originalAny(veryLargeSet, function(){...});

Or more compactly,

_.any()(veryLargeSet, function(){...});

Expressions cannot be used on original methods:

originalAny(veryLargeSet, 'isNumber'); //  => raises exception

The code

Details, code, and examples (including test suite) are on Github.   Released under MIT license.

Update: Just came across Functional.js.  Above technique is not too dissimilar to its string `lambda` functionality.

Written by Moos

April 15th, 2010 at 1:41 pm

Posted in blog

Tagged with ,

Leave a Reply