I am trying to execute following array (avoid callbackHell) of functions in a sequential order implementing function runCallbacksInSequence
(I need to implement
Have the .reduce
callback be a higher-order function, which, when called, calls the next function in the chain with the callback. At the end, you'll have a function chain that will start by calling the first function, then the second, etc:
function first(cb) {
console.log('first()');
cb();
}
function second(cb) {
console.log('second()');
cb();
}
function third(cb) {
console.log('third()');
cb();
}
function last() {
console.log('last()');
}
let fns = [first, second, third, last];
function runCallbacksInSequence(fns, cb) {
const chainedFns = fns.reduceRight((acc, f) => () => f(acc), cb);
return chainedFns();
}
runCallbacksInSequence(fns);
If you wanted the runCallbacksInSequence
to accept another callback to run at the end of all of them, then:
function first(cb) {
console.log('first()');
cb();
}
function second(cb) {
console.log('second()');
cb();
}
function third(cb) {
console.log('third()');
cb();
}
function last(cb) {
console.log('last()');
cb();
}
let fns = [first, second, third, last];
function runCallbacksInSequence(fns, cb) {
const chainedFns = fns.reduceRight((acc, f) => () => f(acc), cb);
return chainedFns();
}
runCallbacksInSequence(fns, () => console.log('outer call'));
fns.reduceRight((acc, f) => f(acc), cb)
runs
[first, second, third, last].reduceRight((acc, f) => f(acc), second)
which turns into
((acc, f) => f(acc))(
((acc, f) => f(acc))(
((acc, f) => f(acc))(
((acc, f) => f(acc))(
second,
last
),
third
),
second
),
first
)
(because that's what reduceRight
does).
The first thing to run is the innermost call,
((acc, f) => f(acc))(
second,
last
)
This becomes
last(second)
which (by the definition of last
) is equivalent to
(function () { console.log('last()'); })(second)
This expression ignores second
, writes last()
to the console, and returns undefined
.
This leaves our expression as
((acc, f) => f(acc))(
((acc, f) => f(acc))(
((acc, f) => f(acc))(
undefined,
third
),
second
),
first
)
The next innermost call is
((acc, f) => f(acc))(
undefined,
third
)
which turns into
third(undefined)
By the definition of third
this is equivalent to
(function (cb) {
console.log('third()');
cb();
})(undefined)
which in turn executes
console.log('third()');
undefined();
This writes third()
to the console, then throws an exception because undefined
is not a function.
Your callbacks never pass arguments, cb()
. In a real program, you will likely want to get a result back. The callback is meant to receive some sort of message - ie, what are you calling back to say? In this program, we'll send some messages out and make sure all of them get passed to the final callback -
function first(cb) {
console.log('first()')
cb(1) // return some result
}
function second(cb) {
console.log('second()')
cb(2) // return some result
}
function third(cb) {
console.log('third()')
cb(3) // return some result
}
function last(cb) {
console.log('last()')
cb('last') // return some result
}
function runCallbacksInSequence(fns, cb) {
fns.reduce
( (r, f) => k => r(acc => f(x => k([ ...acc, x ])))
, k => k([])
)
(cb)
}
const fns =
[ first, second, third, last ]
runCallbacksInSequence(fns, results => {
console.log("-- DONE --")
console.log(...results)
})
The output is -
first()
second()
third()
last()
-- DONE --
1 2 3 'last'
For an extra dose of functional programming -
The reducer above is based on a fundamental data structure called Continuation. If we extract it, we can see what runCallbacksInSequence
is doing more clearly -
function append (a = [], x = null) {
return a.concat([ x ]) // basic append operation
}
function runCallbacksInSequence(fns, cb) {
Cont.run
( fns.reduce // in the context of Cont ...
( Cont.lift2(append) // reduce using append
, Cont.of([]) // init with empty array
)
, cb
)
}
Here's Cont
-
const Cont =
{ of: x =>
k => k (x)
, lift2: f => (mx, my) =>
k => mx (x => my (y => k (f (x, y))))
, run: (c, k) =>
c (k)
}
Expand the snippet below to see the result in your own browser -
function first(cb) {
console.log('first()')
cb(1) // return some result
}
function second(cb) {
console.log('second()')
cb(2) // return some result
}
function third(cb) {
console.log('third()')
cb(3) // return some result
}
function last(cb) {
console.log('last()')
cb('last') // return some result
}
const Cont =
{ of: x =>
k => k (x)
, lift2: f => (mx, my) =>
k => mx (x => my (y => k (f (x, y))))
, run: (c, k) =>
c (k)
}
function append (a = [], x = null) {
return a.concat([ x ])
}
function runCallbacksInSequence(fns, cb) {
Cont.run
( fns.reduce
( Cont.lift2(append)
, Cont.of([])
)
, cb
)
}
const fns =
[ first, second, third, last ]
runCallbacksInSequence(fns, results => {
console.log("-- DONE --")
console.log(...results)
})
Using reduce
is not the only way to express this kind of program. Programming is all about inventing your own convenience. What if we could have an intuitive, magical function like $
below? We could start with some value and then just chain as many steps necessary -
$ ([])
(andAppend(first))
(andAppend(second))
(andAppend(second))
(andAppend(third))
(andAppend(third))
(andAppend(third))
(andAppend(last))
(x => console.log ("done", x))
// first()
// second()
// second()
// third()
// third()
// third()
// last()
// "done" [ 1, 2, 2, 3, 3, 3, "last" ]
Any simple function can go in the sequence -
function progress(p) {
console.log("progress:", p)
return p
}
$ ([])
(andAppend(first))
(andAppend(second))
(progress)
(andAppend(third))
(andAppend(last))
(x => console.log ("done", x))
// first()
// second()
// progress: [ 1, 2 ]
// third()
// last()
// "done" [ 1, 2, 3, "last" ]
This appears to be a very intuitive way to work with our asynchronous functions. We just have to implement $
now. How hard could it be?
const $ = x =>
k => $(Promise.resolve(x).then(k))
And now we implement andAppend
-
function andAppend(f) {
return acc =>
new Promise(r =>
f(x => r([ ...acc, x ]))
)
}
Expand the snippet below to see it working your browser -
function first(cb) {
console.log('first()')
cb(1)
}
function second(cb) {
console.log('second()')
cb(2)
}
function third(cb) {
console.log('third()')
cb(3)
}
function last(cb) {
console.log('last()')
cb('last')
}
function andAppend(f) {
return acc =>
new Promise(r =>
f(x => r([ ...acc, x ]))
)
}
function progress(p) {
console.log("progress:", p)
return p
}
const $ = x =>
k => $(Promise.resolve(x).then(k))
$ ([])
(andAppend(first))
(andAppend(second))
(progress)
(andAppend(third))
(andAppend(last))
(x => console.log ("done", x))