Coding an array of asynchronous functions

Fri Dec 28 2018 14:14:29 GMT+0100 (Hora estándar romance), David Ibáñez

Refactoring an array of asynchronous functions

Lately I'm focusing in improving my code, mainly using more declarative functions like map(), reduce() and filter().

Today, I coded some kind of stuff I usually do, but I changed the way I tried, and I have good and bad feelings about the result. I think it's a clever solution but maybe not so useful for code readability and maintenance.

Today I had to code a process to update settings on a remote standalone device using asyncronous HTTP calls. I work managing remote standalone devices and my main web app have a device manager and can set up things on these devices remotely. The devices itself have a REST API for managing, and my app on the device can call the API using localhost.

How it works

  1. The device asks to the server for settings
  2. The server responses an array of settings to apply
  3. The device app applies that settings. (The number of settings may vary everytime)

What the apply setting process has to do (Point 3 above)

  • Get a setting
  • Send an HTTP Post with that setting to localhost (its' own)
  • Wait for the result from the device
  • If it went ok, go for the next setting
  • When all settings applied then return the result to the server

This means that we need to create an iteration over the array of settings, send the HTTP post, wait for the response and then go for the next item. Because the results are asynchronous we can't just use a for loop nor forEach iteration in a regular way.

I Usually create and index to point where I am in the array and then I call the function again and again increasing the index (A self invoked function).

Let's create the function we will use to apply each setting. I removed ajax details to make things easy.


function sendSetting(token, setting, callback){

    $.ajax({
        // ajax settings
    }).done(function(result){
        if(result && result.value === 'OK'){
            callback();
        } else {
            callback(new Error('Some problem with the setting'));
        }
    }).error(function(err){
        callback(new Error('Error calling the API'));
    });
};

Because some of my remote devices can use a lower version of Android 4.4.4, I can't use promises. I have to use callbacks. I have JQuery on the devices.

What the app needs to do is something like the following code, but not knowing in advance how many times it would have to call sendSetting functions. It will depend on the length of the settings array received.


sendSetting(token, setting, function(err){
    sendSetting(token, setting, function(err){
        sendSetting(token, setting, function(err){
            sendSetting(token, setting, function(err){
                console.log("Settings finished");
            });
        });
    });
});

Code snippet 1

My approach so far was to create a self invoked function iterating through all the indexes of the array keeping an index to know where I was through the iterations and passing all the parameters every time to the function. Something like this.


function applySettings(auth_token, settings, index, callback){

    // Check if we arrived at the end of the array of settings
    if(index >= settings.length){
        callback();
        return;
    }

    // Calling the function defined in the first code snippet
    sendSetting(auth_token, settings[index], function(err){
        if(!err){            
            // Self invoke the function incrementing the index by one
            applySettings(auth_token, settings, ++index, callback);
        else {            
            // If there's an error end the process and invoke the callback
            callback(err);
        }
    });
}

And then a function to initialize the process


function configureSettings(auth_token, settings){
    applySettings(auth_token, settings, 0, function(err){
        if(!err){
            console.log("Everything went well!");
        } else {
            console.log("Ooooops! Something went wrong. " + err.message);
        }
    });
}

configureSettings is the function to initialize the process and receive the result.
applySettings is the function that will operate throught the settings and invoke sendSetting per each setting and invoke the callback when received the response

How I refactored these functions

What I tried to do today is to apply map, reduce. I found that reduce was my best option. What I decided to do is to use reduce method to create a stack of sendSettings functions one as a callback of the next and then invoking the entire thing. As we saw earlier the stack of function will look something like this.


sendSetting(token, setting, function(err){
    sendSetting(token, setting, function(err){
        sendSetting(token, setting, function(err){
            sendSetting(token, setting, function(err){
                console.log("Settings finished");
            });
        });
    });
});

Using reduce I'm able to code all the process just in one simple function. Let's start with step by step implementation.

function configureSettings(auth_token, settings){

    // We reverse the settings. See why below.
    var reversed_settings = settings.reverse()

    // The final callback function
    function processEnd(err){
        console.log('finsished' + (err ? err.message : ''))
    }

    // Preparing the settings functions. 
    var process = reversed_settings.reduce(function(previous_function, setting){
        return sendSetting(token, setting, previous_function);
    }, processEnd);

    // Invoking the whole process
    process();
}

What does this code exactly do

Ok, I think everything is a lilttle confusing right now. Let's try to explain the code line by line.

Why reverse in the first place

    var reversed_settings = settings.reverse()

We need to send a setting, then wait for result and then send the next one. Using reduce we can create a stack of functions as we wanted (see Code snippet 1 ). We need to send setting 1 and set a callback to send setting 2 and set a callback to send setting 3 and so on until send setting n.
Because we need to have the callback of the second function to declare the first one (we need the second as a callback), we need to have the 2nd function declared before declaring the first

Everything it's clear now, isn't it? mmmmm, maybe not. Let's see the picture below

Process.png

As you can see in the picture what we are doing is to prepare the last function, then the parent one, then the parent one.

How we pile up the functions

Then reduce allows as to iterate to all the array of settings from the last to the first (because we reversed the array) and create its send setting function.


    // Preparing the settings functions. 
    var process = reversed_settings.reduce(function(previous_function, setting){
        return sendSetting(token, setting, previous_function);
    }, processEnd);

The array reduce method allows us to:

  • Set an initial value
  • Iterate to each item to pile up it to the initial value
  • After all iterations returns the resulting value

If you don't have a clear idea of what reduce do, you can look this article

On each iteration we receive the previous functions and then set as the callback of the one we're creating on this item.

Process2.png

As parameter to reduce function we pass the first callback of our stack (the last to be called)


    // The final callback function
    function processEnd(err){
        console.log('finsished' + (err ? err.message : ''))
    }

This is the first function we pass to reduce method and the fisrt callback on our stack of functions

Process3.png

Every iteration of the reduce method the functions will be piled up like the image below:

Process4.png

When reduce method ends, we have all the process prepared just to be called. We invoke the process.


    // Invoking the whole process
    process();
}

Now that we know what the function does, let's compact the code a little bit.


function configureSettings(auth_token, settings){
    settings.reverse().reduce(function(previous_function, setting){
        return sendSetting(auth_token, setting, previous_function);
    }, function(err) { console.log('finsished' + (err ? err.message : '')) })();
}

This function does exactly what the step by step version did. We reverse the array, we call the reduce method (using the processEnd function declared directly) and then invoking the function with ().

Finally we need to modify our original sendSetting function with two goals


function sendSetting(token, setting, callback){
    return function(err) {
        if(err) return callback(err);    // call callback and exit function

        $.ajax({
            // ajax settings
        }).done(function(result){
            if(result && result.value === 'OK'){
                callback();
            } else {
                callback(new Error('Some problem with the setting'));
            }
        }).error(function(err){
            callback(new Error('Error calling the API'));
        });
    }
};

In more detail, these are the lines added to the function


    return function(err) {
        if(err) return callback(err);    // call callback and exit function
        ...
    }

Fisrt we modified the sendSetting function to return a function and when calling the method sendSetting in the reduce method we'll receive a function that is what we need for the next callback.

Second, we check if the previous send setting process have failed, if failed we don't execute the further processes and finish the process returning the error.

My thoughts

When first I finished the code and it worked like a charm I thought it was a very improved way to do the thing comparing the way I always did it. Thinking a little bit more, maybe it's not very clear for other developers (and for myself in the future) to understand what exactly the function is doing.

What do you think?


Tags

Follow us

Latest blogs
How to train YOLOv3 using Darknet on Colab notebook and speed up load times
Wed Apr 10 2019 14:57:55 GMT+0200 (Hora de verano romance)
Creating a banking chat bot hosted at IBM Cloud connected to IBM Watson using No
Fri Mar 15 2019 15:24:18 GMT+0100 (Hora estándar romance)
Getting back to #100DaysOfCode
Wed Mar 06 2019 22:30:42 GMT+0100 (Hora estándar romance)
50 days of the 100DaysOfCode challenge
Sun Feb 17 2019 16:47:31 GMT+0100 (Hora estándar romance)
creating your own framework part #2
Tue Jan 29 2019 22:48:31 GMT+0100 (Hora estándar romance)

Latest comments