Sync callbacks

Do not think every callbacks is sync or async.

One thing that always trips me up in JavaScript is its event-driven nature. A piece of code written "normally" does not execute linearly. When we start learning JS language, we see examples like this

1
2
3
4
5
6
7
8
9
function identity (x) {
console.log('x =', x)
return x
}
const result = identity('foo')
console.log('result =', result)

const mapped = [1, 2, 3].map(identity)
console.log('mapped =', mapped)

In what order does this execute? In the order written - from top line to the bottom line. We see this in the printed values

1
2
3
4
5
6
x = foo
result = foo
x = 1
x = 2
x = 3
mapped = [ 1, 2, 3 ]

This is relatively easy to understand. But things become much more complicated when you write "regular" Node or browser code. It is difficult because all the sudden, the callbacks that look absolutely the same are NOT called synchronously. Instead they will be called some time in the future. Here is the "simple" code that causes all the confusion in the world.

1
2
3
4
5
6
7
8
9
10
function asyncIdentity(x, cb) {
setTimeout(() => {
console.log('returning x =', x)
cb(x)
}, 0)
}
const result = asyncIdentity('foo', (arg) => {
console.log('callback value =', arg)
})
console.log('result =', result)

To the outside caller function asyncIdentity looks almost the same as our previous function identity. But look a the program's output - the order of statements is NOT what you would expect by reading the source code.

1
2
3
result = undefined
returning x = foo
callback value = foo

Instead of executing the callback right away, the last statement console.log('result =', result) is executed instead. And notice that we no longer get any result - there is no concept of result available immediately. Instead the computed value will be passed to the callback function by scheduling it to run - to the back of the event queue it goes!

1
2
3
4
setTimeout(() => {
console.log('returning x =', x)
cb(x)
}, 0)

If only this function did not use setTimeout! Then it would not cause any problems in tracing the flow of run time calls.

We get around callbacks. We now got Promises - a way to schedule computation to receive a computed value eventually and call callbacks using .then method, rather than by convention.

1
2
3
4
5
6
7
8
9
10
11
12
function promisedIdentity(x) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('returning x =', x)
resolve(x)
}, 0)
})
}
promisedIdentity('foo')
.then(arg => {
console.log('callback value =', arg)
})

This program acts "more sensibly". The output shows that the code inside promisedIdentity first runs, then the callback inside "then" executes. Since "then" callback is below the promise we almost read the program top to bottom, like a regular synchronous program. This is much better than jumping up and down when reading a source code with callbacks.

Small aside. There is now async / await to force asynchronous code to read "normally". I do not like this construct at all, and would prefer moving towards better structure than Promise - like Task.

Coming full circle, I am now finding myself struggling with using objects like Maybe, all because they use callbacks that I expect to be asynchronous; and I am trying to attach my code to run as a callback, rather than get the value out (like we have done with array const mapped = [1, 2, 3].map(identity)).

For example, if I want to get a valid value, and multiply it by 2, I could have an if statement

1
2
3
4
5
6
7
8
9
const x = ... // comes from somewhere
const double = x => x + x
let result
if (x !== undefined and x !== null) {
result = double(x)
} else {
result = 'default value'
}
console.log(result)

But Maybe is a much better way to work with values that might be invalid.

1
2
3
4
5
const Maybe = require('folktale/maybe')
const x = Maybe.fromNullable(...) // value comes from somewhere
const double = x => x + x
const result = x.map(double).getOrElse('default value')
console.log(result)

Notice how much this looks both like a Promise but the right analogy is Array.map. The callback .map(double) runs synchronously, and the value result is returned synchronously. I often forget it and make my life harder by moving actions to run in a callback

1
2
3
4
5
6
7
// while this works - this is huge overkill
const Maybe = require('folktale/maybe')
const x = Maybe.fromNullable() // value comes from somewhere
const double = x => x + x
x.map(double)
.orElse(_ => Maybe.of('default value'))
.map(console.log)

The above, while works, also runs synchronously, but tries to "fix" the undefined value using .orElse(_ => Maybe.of('default value')) before merging "valid" and "nothing" code paths and calling .map(console.log).

Too bad JavaScript does not tell us (except for now present async keyword) if the callback will be called synchronously vs asynchronously. I have to remember that a lot of times it just runs in sync and the code reads top to bottom.