5

One of the reasons that I tend to dread writing Javascript for anything other than relatively trivial bits of functionality is that I've never found a decent approach for avoiding the callback waterfall when one thing really depends on another. Is there any such approach?

I'm working on a Titanium app right now and bumping into this real world scenario:

I have a set of facilities for which I need to the calculate distance from the user's current location. This entails getting the user's current position (which only needs to happen once) and, while looping through the facility locations, getting the position for each one and calculating the distance. The APIs that retrieve locations (long/lat) are asynchronous so the "easy" approach looks like this (pseudo-code follows):

foreach facility {
  API.getCurrentLocation( function( location ) { // async, takes a callback fxn arg
    var here = location.coordinates;

    API.getFacilityLocation( function( e ) { // async, takes a callback fxn arg
      var there    = e. coordinates;
      var distance = API.calculateFrom( here, there );
    });
  });
}

Because this is all in a loop, though, I'm calculating my current position each time -- more work than I really need to do. I haven't yet managed to refactor this in such a way that I'm only getting the current location once and still having that location available for the distance calculation.

Given the explosion of languages that support lambdas and closures, I keep thinking that someone must have found an approach to keep these waterfalls manageable, but I have yet to find a good explanation of how to organize such a solution.

Any suggestions or hints?

Any insight would be tremendously appreciated.

Rob Wilkerson
  • 37,910
  • 41
  • 130
  • 185
  • 1
    Seems like it should be possible to create a mechanism that works like a "reduce", such that a framework manages the calls and then invokes callbacks with an explicit "context" or something. – Pointy Nov 15 '11 at 13:56
  • 2
    Several implementations of the Promise pattern now. Either use their library or write your own. Promises allow you to chain callbacks together with verbs like "when, done, then" instead of nesting functions. – AutoSponge Nov 15 '11 at 13:59

4 Answers4

3

The basic strategy: don't use anonymous functions for callbacks, and move the loop into the callback that runs when the current location is returned.

Example:

function recieveCurrentLocation(location) { // async, takes a callback fxn arg
    var here = location.coordinates;

    // Have to define this callback in the inner scope so it can close
    // over the 'here' value
    function recieveFacilityLocation(e) {
        var there    = e. coordinates;
        var distance = API.calculateFrom( here, there );
    }

    foreach facility {
        API.getFacilityLocation(recieveFacilityLocation);
    }
}

API.getCurrentLocation(recieveCurrentLocation);
Matt Ball
  • 332,322
  • 92
  • 617
  • 683
  • Yeah, I was kind of avoiding this approach because there's a lot of other non-location-centric stuff happening in that loop that I stripped out for the purpose of the question. I was hoping to avoid munging it all together somehow, but maybe that's just not possible. – Rob Wilkerson Nov 15 '11 at 14:02
  • I've moved back in this direction and here's the catch: the `receiveFacilityLocation` function needs to also accept the row that's being processed in the loop so that it can update the distance display with the result of that function. Every means I've tried still ends up with timing issues and doesn't work properly. – Rob Wilkerson Nov 15 '11 at 16:30
  • There's almost certainly a way to pass the row into `receiveFacilityLocation`. Perhaps you'd like to edit your question or ask a new one that shows a slightly more realistic example. – Matt Ball Nov 15 '11 at 18:02
  • This got me closest and paved the way for the rest. The only "key" change I made is a wrapper function for `receiveFacilityLocation` that accepts the row object being processed. That wrapper function calls `API.getFacilityLocation` and I used an anonymous function for the callback which is now trivial. Thanks for your help. – Rob Wilkerson Nov 15 '11 at 18:43
3

You must start to think more event-oriented. Define function for each callback level and provide it as argument when needed, and don't think of it as callback waterfall. Note that you have the same in each non-batch process: you wait for user actions, there's a big event loop that runs action, which waits for other user action, which runs another event processing action etc.

Simply do what can be done at the moment and for anything asynchronous register handler. User actions and async responses from computer systems are not so different :)

Stepan Vihor
  • 1,059
  • 9
  • 10
1

There are two separate problems here. The first is nesting callbacks (in an "watterfall" manner) and the second is calling an async function without knowing what continutation you want to pas it.

To avoid nesting hell the basic idea is to use names functions instead. So

f1(arg1, function(){
    arg2 = g(arg1);
    f2(function(){
        ...use arg2
    });
});

Can become

var arg2;
f1(arg1, afterf1);

function afterf1(){
    arg2 = g(arg1);
    f2(afterf2);
}

function afterf2(){
    ...use arg2;
}

Note that the only other main refactoring is that we need to move all the variables the inner functions closed over to the outer scope, since the inner functions won't be inner functions anymore (do try to keep shared variables at a minimum - there are many tricks to refactor them into more mantainable code if you feel you are starting to get too many of them).

Now, the other problem is having a callback you don't know when you you use the value of.

In a synchronous case you can do

var x = f();

and whoever wants x can just just access it anytime afterwards.

But in the async case you are limited to doing

f(function(x){
   ...use x here
});

And the only code that will ever be able to see x will be controlled by this callback.

The trick then is having a way to add extra "real" callbacks afterwards and have the callback you passed to the original function just pass on the result to the interested parties, instead of using it directly.

var got_result = false;
var result = null;
var waiting_for_result = [];

function register_callback(f){
    if(got_result){
        f(result);
    }else{
        waiting_for_result.push(f);
    }
}

var real_callback = function(x){
    got_result = true;
    result = x;
    for(var i=0; i< waiting_for_result.length; i++){
        waiting_for_result[i](result);
    }
}

//

API.getCurrentLocation(real_callback);
foreach facility {
    register_callback(function(location){
        ...stuff
    })

Of course, since doing this is a repetitive PITA, there are many Promise libraries that do precisely this. They mostly also have neat methods that allow you do the non-nesting "named callbacks" pattern with anonymous functions as well.

For example, in Dojo this might look like

var location_promise = API.GetLocationPromise();
foreach facility {
    location_promise.then(function(location){
        ...use location
    });
}
hugomg
  • 63,082
  • 19
  • 144
  • 230
0

why dont you define your current location outside the loop ?

var here ;    
API.getCurrentLocation( function( location ) {here = location.coordinates;})

    foreach facility {

        API.getFacilityLocation( function( e ) { // async, takes a callback fxn arg
          var there    = e. coordinates;
          var distance = API.calculateFrom( here, there );
        });
    }

this way you only calculate it once

Sina Fathieh
  • 1,623
  • 1
  • 19
  • 35
  • 2
    Because it's asynchronous. I can't count on it being available when I hit the distance calculation in that scenario. The `location` value is passed to the callback by the API. It's not a local variable I already have. – Rob Wilkerson Nov 15 '11 at 13:58
  • 1
    That will mess up the scope since there is only one `here` variable now which is overwritten each time. – pimvdb Nov 15 '11 at 13:59