Reputation: 3060
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
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
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 Promise
s. 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
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.
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
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