Search code examples
node.jsrecursionsetimmediate

recursion with setImmediate()


I have the following recursive function:

 function explore(v, componentN) {
    return function () {
        nodes[v].visited = true;
        nodes[v].componentNumber = componentN;
        preVisit(v);
        adjList[v].forEach((item) => {
            if (!nodes[item].visited){
                ex(item, componentN)
            }
        });
        postVisit(v);
        return nodes;
    }
}
function ex(item, com){
    trampoline(function () {
        return explore(item, com)
    })
}
function trampoline(fn) {
    while(fn && typeof fn === 'function') {
        fn = fn()
    }
}

I want to use setImmediate() in order to avoid stack overflow issues (it should be implemented in this way and not in the iterative approach). However, when I execute this function I get array res only with the first one element, while I get it full of all elements if I don't use setImmediate() and the same situation appears when I use nextTick() (I know in advance how many elements should be there). What am I doing wrong and how can I implement this function (or analog) properly here?

This is explore() function before applying trampoline

function explore(v, componentN) {
    nodes[v].visited = true;
    nodes[v].componentNumber = componentN;
    preVisit(v);
    adjList[v].forEach((item) => {
        if (!nodes[item].visited){
            explore(item, componentN)
        }
    });
    postVisit(v);
    return nodes;
}

It accepts two arguments: v is an index in nodes array which element is supposed to be explored and componentN which is just a counter of components in a graph. explore() does not return anything, it just changes the state of an object in the array nodes under the v key from unexplored to explored. The main problem is that two functions preVisit(v) and postVisit(v) also change the state of that object - write order on which it was visited for the first time and the second time (when popping up stack from the previous calls) respectively. When I converted explore() with trampoline, they began to write unexpected and wrong results.

The function is being called in another function in this way:

for (let i = 0; i < vNum; i++) {
        if (nodes[i].visited) continue;
        explore(i, cN);
        cN++;
    }

And two function postVisit and preVisit

function preVisit(v){
    nodes[v].preVisit = visitCounter;
    visitCounter++;
}
function postVisit(v) {
    nodes[v].postVisit = visitCounter;
    visitCounter++;
}

Solution

  • Let's say we have an ordinary recursive function like this – the problem here is we have a forEach loop where each call to explore can yield 0, 1, or more additional calls to explore – To fix this, we'll need some way to sequence all of the nodes such that we can process them one-by-one without blowing up the stack

    const explore = node =>
      {
        node.visited = true
        preVisit (node)
        Array.from (node.adjacencies)
          .filter (n => n.visited === false)
          .forEach (n => explore (n))
        postVisit (node)
      }
    

    We introduce a main loop inside the explore function which processes an array of nodes, instead of just a single node. The loop will continue to run while there are nodes in the array. Recursive calls will be made to the inner loop function instead of the outer explore function. This array approach works nicely because each node has an ambiguous amount of adjacencies – when can easily add 0, 1, or more adjacent nodes to this list – also note the continuation k is added so we have a way to sequence the postVisit calls in the correct order

    Of most primary importance tho is the recursive call to loop is in tail position – which prepares us for the next transformation

    const explore = node =>
      {
        const loop = ([x, ...xs], k) =>
          {
            if (x === undefined)
              k ()
            else {
              x.visited = true
              preVisit (x)
              loop (
                Array.from (x.adjacencies)
                  .filter (n => n.visited === false)
                  .concat (xs),
                () => (postVisit (x), k ())
              )
            }
          }
        return loop ([node], x => x)
      }
    

    Once we have a tail-recursive loop, we can introduce loop/recur for stack-safe recursion

    const recur = (...values) =>
      ({ type: recur, values })
    
    const loop = f =>
      {
        let acc = f ()
        while (acc && acc.type === recur)
          acc = f (...acc.values)
        return acc
      }
    
    const explore = $node =>
      loop (([x,...xs] = [$node], k = x => x) => {
        if (x === undefined)
          return k ()
        else {
          x.visited = true
          preVisit (x)
          return recur (
            Array.from (x.adjacencies)
              .filter (n => n.visited === false)
              .concat (xs),
            () => (postVisit (x), k ())
          )
        }
      })
    
    // for completion's sake
    let visitCounter = 0
    
    const preVisit = node =>
      node.preVisit = visitCounter++
    
    const postVisit = node =>
      node.postVisit = visitCounter++