Thursday, July 8, 2010

Underscore.js and the End of Accumulators

‹prev | My Chain | next›

I ended up yesterday's session with a test that looks like:
       'send 1000+ bytes to get Chrome\'s attention': function(chunks) {
var byte_count = 0;
for (var i=0; i < chunks.length; i++) {
var chunk = chunks[i];
if (chunk && chunk.body && typeof(chunk.body) == "string") {
byte_count = byte_count + chunk.body.length;
}
}

assert.isTrue(byte_count > 1000);
},
That works just fine, but I am not fond of accumulators and index tracking variables.

To get cool, ruby-like iterators I install underscore.js. Happily, I can install via npm:
cstrom@whitefall:~/repos/my_fab_game$ npm install underscore
The "ini" module will be removed in future versions of Node, please extract it into your own code.
npm configfile /home/cstrom/.npmrc
npm sudo false
npm cli [ 'install', 'underscore' ]
npm install pkg underscore
npm fetch data underscore
npm GET underscore
npm install pkg underscore
npm install pkg underscore@1.0.3
...
npm activate underscore 1.0.3
npm readJson /home/cstrom/.node_libraries/.npm/underscore/active/package/package.json
npm readJson /home/cstrom/.node_libraries/.npm/underscore/1.0.3/package/package.json
npm build Success: underscore-1.0.3
npm ok
The current version of underscore.js is 1.04, but I will not quibble given the convenience. Since I can install it via npm, I ought to be able to require it the usual way:
var _ = require('underscore');
Then, I can convert from accumulators/trackers to:
      'send 1000+ bytes to get Chrome\'s attention': function(chunks) {
var byte_count = _(chunks).
filter(function(chunk) {
return chunk && (typeof(chunk.body) == "string");
}).
map(function(chunk) {
return chunk.body.length;
}).
reduce(function(memo, length) {
return memo + length;
});

assert.isTrue(byte_count > 1000);
},
I wrap the chunks of HTTP responses inside an underscore object, then filter out only those chunks with a body (non-headers), map each to the size of the response, then reduce each to a simple sum. Easy-peasey except... it does not work:
cstrom@whitefall:~/repos/my_fab_game$ vows --spec

♢ init_comet

with a player
✓ sets a session cookie
✓ sends the opening HTML doc
✗ send 1000+ bytes to get Chrome's attention
TypeError: object is not a function
at Object.CALL_NON_FUNCTION (native)
at Object.<anonymous> (/home/cstrom/repos/my_fab_game/test/init_comet.js:47:26)
at runTest (/home/cstrom/.node_libraries/.npm/vows/0.4.5/package/lib/vows.js:99:26)
at EventEmitter.<anonymous> (/home/cstrom/.node_libraries/.npm/vows/0.4.5/package/lib/vows.js:72:9)
at EventEmitter.emit (events:42:20)
at /home/cstrom/.node_libraries/.npm/vows/0.4.5/package/lib/vows/context.js:24:44
at EventEmitter._tickCallback (node.js:48:25)
at node.js:204:9
Ah, looking through the underscore code a bit, it looks as though it takes care of exporting the underscore to the global namespace itself, so I change my require to simply read:
require('underscore');
And then my tests pass again:
cstrom@whitefall:~/repos/my_fab_game$ vows --spec

♢ init_comet

with a player
✓ sets a session cookie
✓ sends the opening HTML doc
✓ send 1000+ bytes to get Chrome's attention
✓ sends the player
without a player
✓ terminates the downstream connection
with an invalid player object
✓ terminates the downstream connection

♢ player_from_querystring

with a query string
✓ is player
✓ has unique ID
✓ has X coordinate
✓ has Y coordinate
without explicit X-Y coordinates
✓ has X coordinate
✓ has Y coordinate
POSTing data
✓ is null response

✓ OK » 13 honored (0.122s)
As I was reading through the underscore.js source code, I could not help noticing many comments along the lines of:
  // Delegates to JavaScript 1.6's native filter if available.
In fact, each of the three methods that I used, filter, map, and reduce have comments like that. I am using v8, which means that I ought to have the latest and greatest ECMAscript standards, so why use underscore.js at all?
      'send 1000+ bytes to get Chrome\'s attention': function(chunks) {
var byte_count = chunks.
filter(function(chunk) {
return chunk && (typeof(chunk.body) == "string");
}).
map(function(chunk) {
return chunk.body.length;
}).
reduce(function(memo, length) {
return memo + length;
});

assert.isTrue(byte_count > 1000);
},
Sure enough, that works just fine. I would note that this solution is not any shorter than the accumulator / index variable version. It would also clean things up a bit if Javascript did not require the function or return statements. Nevertheless, I am much more comfortable with this version. Each iterator is self-contained which prevents them from being at the mercy of out-of-scope variables. Such variables are not a problem now, but in the future, they suck for maintainability.

Ah, well. Good to know that I can get it into node.js / fab.js / vows if I need it. Even better to know that I do not need to add an extra dependency just yet.


Day #158

1 comment:

  1. Just to let you know: you were never using underscore's version of map or reduce.

    Underscore only returns a wrapped object if you call chain() first. Then you'd have to call value() at the end to get the original one back. Normally it would just return an array or value, which has no underscore methods.

    ReplyDelete