JavaScript: Calling back from inside callbacks & recursion

Let’s say you have an array of arrays:

var a = [1, 2, 3];
var b = [4, 5, 6];
var c = [7, 8, 9];
var abc = [a, b, c];

You can define a callback:

var lessThan5 = function (val) { return val < 5; };

and then call it on any of the single-letter arrays:

a.every(lessThan5);  // true
c.every(lessThan5);  // false

But what if what you really want to do is test whether every atomic value in the three subarrays held by abc is less than 5? You could (incorrectly) try this:

abc.every(lessThan5);  // false, but not testing what you believe

To see that it’s not working as we want it to, let’s create this callback:

var greaterThan0 = function (val) { return val > 0; };

Since every value in every array held by abc is > 0, we want this callback to return true, but it still returns false:

abc.every(greaterThan0);  // false

abc.every is looping over an array of arrays, not an array of primitives (integers). What we really want is to have the callback called once for each array in abc call array.every(greaterThan0) on whatever array it receives. We need an intermediate callback, or a callback that calls .every() with another callback. How might we do this?

abc.every(function (array) { return array.every(greaterThan0); });  // true!

This works. We can prove this by adding a fourth array holding a negative value and watching the function return false:

d = [-1, 10, 11];
abc.every(function (array) { return array.every(greaterThan0); });  // false

But this is messy and gets even messier if you add another layer (or more) of callbacks. How can we clean this up?

Instead of using a function literal as our first callback, we can store the function in a named variable and pass in the variable:

var arrayGreaterThan0 = function (array) { return array.every(greaterThan0); };

To summarize:

var greaterThan0 = function (val) { return val > 0; };   // inner callback
var arrayGreaterThan0 = function (array) {               // outer callback
  return array.every(greaterThan0);
abc.every(arrayGreaterThan0);                            // invocation on abc

What if we don’t know till runtime how many levels down into subarrays we must descend before we hit primitive values? Then the callback can’t know in advance whether it’s being handed an array of arrays or an array of primitives. But it can test what it has been handed and either call itself (if it has received an array of arrays) or call greaterThan0 (if handed an array of primitives):

var greaterThan0 = function (val) { return val > 0; };   // leaf-level callback
var arrayGreaterThan0 = function (array) {               // recursive outer callback
  if (Array.isArray(array[0])) {
    return array.every(arrayGreaterThan0);
  } else {
    return array.every(greaterThan0);
abc.every(arrayGreaterThan0);  // false

If arrays are wrapped inside arrays inside other arrays, it will recursively descend down and down, however far it needs to, till it hits primitive values.

To make sure this works correctly for our array of arrays, let’s change a value and confirm the result changes:

abc[3][0] === -1;  // true
abc[3][0] = 43;
abc.every(arrayGreaterThan0);  // true

Now let’s test the recursion against an array of arrays of arrays:

var aaa = [[[1,2],[3,4]],[[5,6],[7,8]]];
aaa.every(arrayGreaterThan0);  // true

aaa[1][1][1] = -10;
aaa.every(arrayGreaterThan0);  // false

But what if the original array is simply an array of integers? It fails!

simple_array = [1, 2, 3];
simple_array.every(arrayGreaterThan0);  // "TypeError: Object 1 has no method 'every'"

We’ve assumed the original array is at least an array of arrays. If it’s a simple array, the callback fails. How can we correct this? Instead of assuming we received an array and testing what its child is, we can test what we’re actually handed and respond accordingly:

var greaterThan0 = function (val) { return val > 0; };   // leaf-level callback
var objGreaterThan0 = function objGreaterThan0(obj) {    // recursive outer callback
  if (Array.isArray(obj)) {                              //   not yet at leaf level
    return obj.every(objGreaterThan0);                   //     recurse
  } else {                                               //   at leaf level
    return greaterThan0(obj);                            //     call leaf-level callback

simple_array = [1, 2, 3];
simple_array.every(objGreaterThan0);    // true

simple_array.every(objGreaterThan0);    // false

And it still works with arrays inside arrays inside arrays:

var aaa = [[[1,2],[3,4]],[[5,6],[7,8]]];
aaa.every(objGreaterThan0);             // true

aaa[1][1][1] = -10;
aaa.every(objGreaterThan0);             // false

This is probably not the cleanest, most efficient solution, but it works. More to come.

Posted by James on Friday, March 22, 2013