Calling an array of functions, each of which receives a callback

I have a series of functions that take a callback and need to feed each other, each in turn, and a "main" function that also takes a callback. this.app

refers to a member of the class (es6). I want to replace with async call from async module using modern es6 tools:

firstFunction(next){
  if(this.app.isValid()) {
    next(null, app);
  } else {
    next(thia.app.sendError(), null)
  }
}

secondFunc(next){
    this.app.data = {
       reader: "someone"
    };
    next(null, this.app);
}

thirdFunc(next){
    next(null, this.app);
}

majorStuff(next){
    //USING async module (I don't want that)
    // async.series([
    //    firstFunction,
    //     secondFunction,
    //    thirdFunction
    // ], (err, result) => {
    //  if(err) {
    //      next({
    //          success: false,
    //          message: err
    //      })
    //  } else {
    //      next({
    //          success: true,
    //          message: "Welcome to Mars!"
    //      })
    //  }
    // });

    <using babel-polyfill, stage-0 + es2015 presets instead>
}

      

+3


source to share


4 answers


I have a series of functions that take a callback and need to feed each other, each in turn

But you wrote your functions stupidly. How can they feed each other if each only accepts a callback? To create a common flow of data from one function to another, each function must be written in a uniform way. Let's review your function first

// only accepts a callback
firstFunction(next){
  // depends on context using this
  if(this.app.isValid()) {
    // calls the callback with error and successful value
    next(null, app);
  } else {
    // calls the callback with error and successful value
    next(this.app.sendError(), null)
  }
}

      

We would like to make this generic so that we can chain many functions together. Perhaps we could come up with some kind of interface similar to this

// where `first`, `second`, and `third` are your uniform functions
const process = cpscomp (first, second, third)

process(app, (err, app) => {
  if (err)
    console.error(err.message)
  else
    console.log('app state', app)
})

      

This answer exists, if anything, to show you how much work you have to write in a forwarding style - and more importantly, how much work with Promises saves you attention. This is not to say that CPS has no precedent, simply because it probably shouldn't be your turn for asynchronous control flow.


baby steps

I like to work with a minimal amount of code, so I can see how everything will fit together. Below we have 3 approximate function ( first

, second

, third

), and a function that is their association with each other compcps

(which means style Continuation)

const first = (x, k) => {
  k(x + 1)
}

const second = (x, k) => {
  k(x * 2)
}

const third = (x, k) => {
  k(x * x * x)
}

const compcps = (f, ...fs) => (x, k) => {
  if (f === undefined)
    k(x)
  else
    f(x, y => compcps (...fs) (y, k))
}

const process = compcps (first, second, third)

process(1, x => console.log('result', x))
// first(1, x => second(x, y => third(y, z => console.log('result', z))))
// second(2, y => third(y, z => console.log('result', z)))
// third(4, z => console.log('result', z))
// console.log('result', 64)
// result 64
      

Run codeHide result



Node continuation walkthrough

Node adds a layer of convention on top of this by passing in the error (if any) callback first. To support this, we only need to make minor changes to our function compcps

- (changes in bold )

const compcps = (f,...fs) => (x, k) => {
  if (f === undefined)
    k(null, x)
  else
    f(x, (err, y) => err ? k(err, null) : compcps (...fs) (y, k))
}

const badegg = (x, k) => {
  k(Error('you got a real bad egg'), null)
}

const process = compcps (first, badegg, second, third)

process(1, (err, x) => {
  if (err)
    console.error('ERROR', err.message)
  else
    console.log('result', x)
})
// ERROR you got a real bad egg
      

The error goes straight to our postback process

, but we must be careful! What if there is a sloppy function that throws an error but does not pass it to the first callback parameter?

const rottenapple = (app, k) => {
  // k wasn't called with the error!
  throw Error('seriously bad apple')
}

      



Let's make a final update to our function compcps

that will correctly recode these errors into callbacks so that we can handle them correctly - (changes to bold )

const compcps = (f,...fs) => (x, k) => {
  try {
    if (f === undefined)
      k(null, x)
    else
      f(x, (err, y) => err ? k(err, null) : compcps (...fs) (y, k))
  }
  catch (err) {
    k(err, null)
  }
}

const process = compcps (first, rottenapple, second, third)

process(1, (err, x) => {
  if (err)
    console.error('ERROR', err.message)
  else
    console.log('result', x)
})
// ERROR seriously bad apple

      


Using compcps

in your code

Now that you know how your functions should be structured, we can write them easily. In the code below, instead of relying on context sensitive this

, I will pass app

as state that passes from function to function. The entire sequence of functions can be well expressed with a single call compcps

, as you see in main

.

Finally, we run main

with two state variables to see different results

const compcps = (f,...fs) => (x, k) => {
  try {
    if (f === undefined)
      k(null, x)
    else
      f(x, (err, y) => err ? k(err, null) : compcps (...fs) (y, k))
  }
  catch (err) {
    k(err, null)
  }
}

const first = (app, k) => {
  if (!app.valid)
    k(Error('app is not valid'), null)
  else
    k(null, app)
}

const second = (app, k) => {
  k(null, Object.assign({}, app, {data: {reader: 'someone'}}))
}

const third = (app, k) => {
  k(null, app)
}

const log = (err, x) => {
  if (err)
    console.error('ERROR', err.message)
  else
    console.log('app', x)
}

const main = compcps (first, second, third)
  
main ({valid: true}, log)
// app { valid: true, data: { reader: 'someone' } }

main ({valid: false}, log)
// ERROR app is not valid
      

Run codeHide result



Notes

As others have commented, your code is only doing synchronous actions. I'm sure you've simplified your example (which you shouldn't be doing), but the code I've provided in this answer can run completely asynchronously. Whenever it is called k

, the sequence continues to the next step - whether called k

synchronously or asynchronously.

All of the above says that the continuation style is not without headaches. There are many little traps in there.

  • What if the callback is never called? How do we debug the problem?
  • What if the callback is called multiple times?

Many people have switched to using Promises to handle asynchronous control flow; especially since they are fast, stable and natively supported by Node for quite some time. Of course, the API is different, but it aims to reduce some of the stress that exists with heavy cps use. Once you learn how to use Promises, they start to feel completely natural.

In addition, it async/await

is a new syntax that greatly simplifies all templates that ship using Promises. Finally, asynchronous code can be very flat, like its synchronous counterpart.

There's a huge push towards Promises, and the community is behind it. If you're stuck with writing CPS it's good to learn some of these techniques, but if you're writing a new application, I would ditch CPS in favor of API Promises sooner rather than later.

+1


source


You can just mimic the async.series interface:

function series(fns, cb) {
  const results = [];

  const s = fns.map((fn, index) => () => {
    fn((err, result) => {
      if (err) return cb(err, null);
      results.push(result);
      if (s[index + 1]) return setImmediate(s[index + 1]);
      return cb(null, results);
    });
  });

  s[0]();
}

      



Then call it like this:

series([
  first,
  second,
  third
], (err, results) => console.log(err, results));

      

+2


source


If your functions are asynchronous, then consider coordinating with a function generator:

// Code goes here
var app = {};

function firstFunction(){
  if(isValid(app)) { 
    setTimeout(function(){
      gen.next(app); 
    }, 500);
  } else {
    gen.next(null); 
  }
  function isValid(app) {
    return true;
  }
}

function secondFunc(app){
    setTimeout(function(){
      app.data2 = +new Date();
      gen.next(app); 
    }, 10);
}

function thirdFunc(app){
    setTimeout(function(){
      app.data3 = +new Date();
      gen.next(app); 
    }, 0);
}

function* majorStuff(){
  var app = yield firstFunction();
  app = yield secondFunc(app);
  app = yield thirdFunc(app);
  console.log(app);
}

var gen = majorStuff();
gen.next();

      

0


source


Basically, just looking at the example, I see no reason to use anything related to asynchronous connection. But if you want to reproduce it with async

- await

, then here's how to do it:

First convert your methods so that they return Promise

s. Promises are either allowed with a value or rejected with error.

const firstFunction() {
  return new Promise((resolve, reject) => {
    if(this.app.isValid()) {
      resolve(app)
    } else {
      // assuming sendError() returns the error instance
      reject(thia.app.sendError()) 
    }
  })
}

secondFunc() {
  return new Promise(resolve => {
    this.app.data = {
      // its not a good idea to mutate state and return a value at the same time
      reader: "someone"  
    }
    resolve(this.app)
  })
}

thirdFunc(){
  return new Promise(resolve => resolve(this.app))  
}

      

Now, when you return your functions, you can either wait for them in an async function:

async majorStuff() {
  try {
    await Promise.all(
      this.firstFunction(),
      this.secondFunc(),
      this.thirdFunc()
    )
    return { success: true, message: "Welcome to Mars!" }
  } catch(e) {
    return { success: false, message: e.message }
  }
}

      

Or use them like regular Promises:

const result = Promise.all(
  this.firstFunction(),
  this.secondFunc(),
  this.thirdFunc()
).then(() => ({ success: true, message: "Welcome to Mars!" }))
 .catch(e => ({ success: false, message: e.message }))

      

If you want an external API to be able to connect to your methods, you can use these subparts now to do it however you want.

If you want to make sure your Promises are running in sequence, you can do something like this:

const runSeries = (promiseCreators, input) => {
  if (promiseCreators.length === 0) {
    return Promise.resolve(input)
  }
  const [firstCreator, ...rest] = promiseCreators
  return firstCreator(input).then(result => runSeries(rest, result))
}

runSeries([
  input => Promise.resolve(1 + input),
  input => Promise.resolve(2 + input),
  input => Promise.resolve(3 + input),
], 0).then(console.log.bind(console)) // 0 + 1 => 1 + 2 => 3 + 3 => 6

      

The function runSeries

takes an array of promise makers (functions that return a promise) and runs them starting from the given input, then the result of the previously promised promise. This is as close to async.series

. You can obviously tweak it to your needs to handle the arguments better.

-2


source







All Articles