Friday, March 18, 2011

High order loop function in Javascript

Writing Javascript code after Groovy code makes you wish you could extend the language. So I tried to write a Javascript function to rewrite the 'for' loop in a different way, passing the loop parameters and a function to my new 'loop' function. Here are a few ways of doing it, the difference is how the 'for' parameters are passed to the 'loop' function. I wish this was integrated in jQuery, that would make some code more fluid than a 'for' loop in the middle of jQuery code. The next step could be to add the little bit of code to make it a jQuery extension.

OBJECTIVE:
I want to replace a *normal* for loop like this:

var res = []; // array to hold all the results of the fn(args) calls
for (var i = 0 ; i < max ; i + step) res.push(fn(args)); // loop and call the fn(args) function, collecting the results in the res array

by something a bit less verbose like that:

var res = loop(0, max, step, fn, args);

First try:

Passing the parameters in an array [start, end, increment] and the function to call as the second argument. The argument for the function is not there, because I worked out a second form that I like better before including the argument for the function.

Params:
p : number or array

- if number: end index value. The start index is set to 0 and the increment to 1.
- if array of numbers:
p[0] = loop index start
p[1] = loop index end + 1
p[2] = loop index increment

fn: function to apply in the loop. The function receives the loop index as its argument

Result:
Array of results returned by the function

Loop function definition:

function loop(p, fn)
{
var ar = [];
for (var k = (p[0] || 0) ; k < (p[1] || p) ; k += (p[2] || 1))
{
var r = fn(k);
if (r !== undefined) ar.push(r);
}
return ar; // return an array of the function results
}

Examples:

var fn1 = function(index)
{
alert("fn1 index: " + index);
return (index);
}

var res = loop(4, fn1);
alert("fn1 result: " + res); // [0,1,2,3]

res = loop([1, 10, 2], fn1);
alert("fn1 result: " + res); // [1,3,5,7,9]


Second try: to avoid the array for the loop parameters, the loop function can be written like the following. Parameters for the function have been added as well.

function nloop(_s, _e, _i, _f, _c, _a0, _a1, _a2)
{
// _a0, _a1, _a2 are the argument to pass to the function, on top of the index. Optional
// Default if all arguments are specified
// call arguments: (start, end, step, function [,context [arg0 [, arg1 [, arg2]]]]).
var c; // function context: 'this' for the function. Optional
var f; // function to call, required
var i; //  step, loop increment. Optional
var e; // loop end. Optional
var s; // loop count or loop start if _e is defined. Required
var ar = []; // result, array of results from the function _f calls

if (_f instanceof Function)
{
c = _c;
f = _f; // function to call
i = _i; //  step
e = _e; // end
s = _s; // loop count or loop start if _e is defined. Required
}
// call arguments: (start, end, function, context). Default step = 1.
else if (_i instanceof Function)
{
c = _f;
f = _i; // function to call
i = 1; //  step
e = _e; // end
s = _s; // loop count or loop start if _e is defined. Required
}
else if (_e instanceof Function)
{
c = _i;
f = _e; // function to call
i = 1; //  step
e = _s; // end
s = 0; // loop count or loop start if _e is defined. Required
}
else return ar;
// alert('start: ' + s + ' end: ' + e + ' step: ' + s);
// var undef; // to test if the function result is undefined
for (var k = s ; k < e ; k += i)
{
var r = f.call(c, k, _a0, _a1, _a2);
// if (!(r === undef)) // looks like it's working in FF and IE6. Is it a proper way to test for undefined?
if (typeof(r) !== 'undefined') // would it be a bit slow to test for a string?
{
// alert('f result: ' + r);
ar.push(r);
}
}
return ar;
}

Examples of calls:

var fn2 = function(index, a, b, c)
{
// check the parameters and the context
alert("fn2 index: " + index + ' a: ' + a + ' b: ' + b + ' c: ' + c + ' this.name: ' + this.name);
return (index);
}

// An object to pass as the context (aka 'this') for the function called in the loop
var anobject = { name: 'an Object' };

// loop from 1 to 10 step 2, calling fn2 with 'anobject' as 'this' and with 'a', 'b' and 'c' as parameters to fn2
res = nloop(1, 10, 2, fn2, anobject, 'a', 'b', 'c');
alert('nloop fn2 result: ' + res);

res = nloop(1, 3, 1, fn2, anobject, 'a', 'b', 'c');
alert('nloop fn2 result: ' + res);

// function parameters are optional
res = nloop(1, 10, 2, fn2, anobject);
alert('nloop fn2 result: ' + res);

// step optional
res = nloop(1, 5, fn2, anobject);
alert('nloop fn2 result: ' + res);

// start index optional, 4 is the end index + 1
res = nloop(4, fn2, anobject);
alert('nloop fn2 result: ' + res);

var fn3 = function(index)
{
alert('fn2 index: ' + index);
return index;
}
res = nloop(1, fn3, anobject);
alert('nloop fn3 void result: ' + res + ' res == []: ' + (res == []) + ' res == null: ' + (res == null) + ' res.length: ' + res.length);