Chen
Chen

Reputation: 3060

Calling an array of functions, each receive a callback

I have series of functions that accept a callback, and should feed each other, each one in its turn, and a "major" function which accepts a callback as well. this.app refers to a member of a class (es6). I wish to replace to async call from the async module, with the modern tools of es6:

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>
}

Upvotes: 3

Views: 140

Answers (4)

lxe
lxe

Reputation: 7599

You can simply imitate 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));

Upvotes: 2

Bal&#225;zs &#201;des
Bal&#225;zs &#201;des

Reputation: 13807

Basically by just looking at the example, I see no reason to use anything async related. But if you want to reproduce this using async-await then here is a way to do it:

First transform your methods, so that they return Promises. Promises either resolve with a value, or reject with an 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 that you have your promise returning functions you can either await 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 as 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 hook into your methods, then you can use these composable pieces now to do that however you want.

If you want to make sure that your Promises run in a sequence, you could 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-creators (functions that return a promise) and runs them starting with the given input then the result of the previously ran promise. This is as close as it gets to async.series. You can obviously tweak it to your needs to handle arguments better.

Upvotes: -2

Mulan
Mulan

Reputation: 135227

I have series of functions that accept a callback, and should feed each other, each one in its turn

But you wrote your functions in a silly way. How can they feed one another if each one only accepts a callback? In order to create a generic flow of data from one function to another, each function needs to be written in a uniform way. Let's first review your function

// 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'd like to make this generic such that we can assemble many functions in a chain. Perhaps we could come up with some interface that looks like 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 it is to write with continuation passing style – and maybe more importantly, how much work using Promises saves you. That's not to say CPS doesn't have a use case, just that it probably shouldn't be your go-to for async control flow.


baby steps

I like to get stuff working with minimal amount of code, so I can see how everything will fit together. Below we have 3 example functions (first, second, third) and a function that's mean to chain them together, compcps (which stands for compose continuation passing style)

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


Node-style continuation passing

Node adds a layer of convention on top of this by passing an Error first (if present) to the callback. To support this, we only need to make a minor change to our compcps function – (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 passes straight through to our process callback, but we must be careful! What if there's a negligent function which throws an error but does not pass it to the callback's first 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 compcps function that will properly funnel these errors into the callbacks so that we can handle them properly – (changes in 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 must be structured, we can write them with ease. In the code below, instead of relying upon context-sensitive this, I will be passing app as the state that moves from function to function. The entire sequence of functions can be nicely expressed using a single compcps call as you see in main.

Lastly, we run main with two varying states to see the different outcomes

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


Remarks

As others have commented, your code is only doing synchronous things. I'm certain that you've over-simplified your example (which you shouldn't do), but the code I've provided in this answer can run entirely asynchronously. Whenever k is called, the sequence will move onto the next step – whether k is called synchronously or asynchronously.

All things said, continuation passing style is not without its headaches. There's a lot of little traps to run into.

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

Many people have moved to using Promises for handling asynchronous control flow; especially since they've fast, stable, and natively supported by Node for quite awhile now. The API is different of course, but it aims to relieve some of the stresses that exist with heavy use of cps. Once you learn to use Promises, they start to feel quite natural.

Furthermore, async/await is a new syntax that dramatically simplifies all of the boilerplate that comes with using Promises – finally async code can be very flat, much like its synchronous counterpart.

There's a huge push in the direction of Promises and the community is behind it. If you're stuck writing CPS, it's good to master some of the techniques, but if you're writing a new app, I would abandon CPS in favour of a Promises API sooner than later.

Upvotes: 1

Alex Pollan
Alex Pollan

Reputation: 873

If your functions are asynchronous then consider the coordination via 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();

Upvotes: 0

Related Questions