Monday, May 20, 2013

Writing More Functional Javascript with UnderscoreJS

I scrolled through some of my code recently and realized that not once in the 500 lines that I wrote did I use a for loop. In fact, I rarely ever do any longer. Between jQuery's each/map and the plethora of functions available in Underscore, there's just no need to use loops to iterate over objects and arrays. In general, code becomes easier to write and read when using discrete components to apply transforms to manipulate data contained in arrays and object hashes. However, it is also possible to get a little carried away and attempt to do everything through these functions where maybe one iterator will probably be sufficient.

Generally, the balance is based on simplicity. Converting an object hash to a string suitable for a URL query string seems like a reasonable transform to leverage Underscore's library of functions. Compared to its more imperative counter-part which would require several intermediate variables to track state, this version neatly tucks those details away inside the map() function:


queryString: function (params) {
return _.map(params, function (v,k) { return k+'='+encodeURIComponent(v); }).join('&');
}


Granted, internally, Underscore may revert to using a for loop to iterate over the object, but that's an implementation detail you don't need to worry about.

To keep it simple, the above function doesn't take into account converting an array into a repeating set of key/value pairs. In fact, the reverse process that does account for this scenario is a little more difficult to write as a chained set of Underscore calls. Here's a version that uses a for loop and no other Underscore function to parse a query string into an object hash:


function parseQuery ( query ) {

var pairs = (query || "").split('&'),
obj={};

for( i=0;i<pairs.length;i++ ) {

var p = pairs[i].split('=');

p[1] = decodeURIComponent(p[1] || '');

if ( obj[p[0]] ) {
obj[p[0]] = _.isArray(obj[p[0]]) ? obj[p[0]].concat([p[1]]) : [obj[p[0]], p[1]];
} else {
obj[p[0]] = p[1];
}
}

return obj;

}


There seems little room for leveraging much of Underscore to accomplish the task. The most obvious choice is to replace the for loop with an each() function:


function parseQuery2( query ) {

var obj = {};

_.each((query || "").split('&'),

function (pairs) {

var p = pairs.split('=');

p[1] = decodeURIComponent(p[1] || '');

if ( obj[p[0]] ) {
obj[p[0]] = _.isArray(obj[p[0]]) ? obj[p[0]].concat([p[1]]) : [obj[p[0]], p[1]];
} else {
obj[p[0]] = p[1];
}

});

return obj;
}


And if you're really bothered by the if/else needed to detect the repeating key, you could just use an array for everything and remove it later through a call to map:


function parseQuery2( query ) {

var obj = {};

_.each((query || "").split('&'),
function (pairs) {

var p = pairs.split('=');

p[1] = decodeURIComponent(p[1] || '');

obj[p[0]] = (obj[p[0]] || []).concat([p[1]]);

});

// Pull single values out of the array ( ie { a: ["1"] } )
return (
_.chain(obj)
.map(function (v, k) { return [k, v.length == 1 ? _.first(v) : v]; })
.object()
.value()
);
}



Whether that's better than just sticking with the if/else might be a matter of opinion. It seems like introducing two more loops, even if they do run in linear time, just adds more work that could otherwise be accomplished in one iterator.

I've found that transforms that follow through distinct steps that can be chained together are really good candidates for use with Underscore's library. Consider a situation where you need to modify the names of the keys in an object. Generally, modifying the values is quite straight forward but the other way around is a bit harder. Using the following simple object:



{
pre_a: "1",
pre_b: "2",
xpre_c: "3"
}




We don't want the pre/xpre part and need to find a way to rebuild the object with new keys. An easy way to achieve this with Underscore is to convert the object to an array of arrays, perform the replacement, and then convert back to an object:



function modifyKeys( obj) {

return (
_.chain(obj || {})
.pairs()
.map(function (p) { return [ p[0].replace(/^[x]?pre_/, ''), p[1] ]})
.object()
.value()
);
}



The intermediate output from each step using the example object would look like this:



pairs() => [ ["pre_a", "1"], ["pre_b", "2"], ["xpre_c", "3"] ]
map() => [ ["a", "1"], ["b", "2"], ["c", "3"] ]
object() => { a: "1", b: "2", c: "3" }



Thinking functionally may not always be easy, however, the benefits of becoming proficient with the concepts and the library of functions provided by Underscore can significantly reduce the amount of effort necessary to manage object and array transformations. Need and experience generally leads to better comprehension of the problems that can best leverage a given solution. If you haven't tried it yet, take a look at your code and attempt to convert a loop iterating over an object/array and see if an Underscore function or two can reduce the amount of code required to accomplish the manipulation.