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 | function identity (x) { |
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 | x = foo |
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 | function asyncIdentity(x, cb) { |
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 | result = undefined |
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 | setTimeout(() => { |
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 | function promisedIdentity(x) { |
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 | const x = ... // comes from somewhere |
But Maybe
is a much better way to work with values that might be invalid.
1 | const Maybe = require('folktale/maybe') |
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 | // while this works - this is huge overkill |
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.