It strikes me that developers in JS community tend to choose patterns for solving recurring problems over abstractions.
If we are not busy with semicolon debates, we argue about widely used “callback pattern” for dealing with asynchronous API. Many have learned / invented ways to avoid “pyramids of doom”, but I believe they miss the point: pyramids are not the issue, it’s an indication that we have one.
In order to describe what I consider to be a real issues, I have to move back a little first:
In mathematics, a function is a relation between a set of inputs and a set of potential outputs with the property that each input is related to exactly one output.
In science and engineering, a “black box” abstraction is used to model systems as set of components which can be viewed solely in terms of its input, output and transfer characteristics, without any knowledge of its internal workings. This components are opaque (black) boxes.
Bigger boxes can be created out of smaller ones just by describing a data flow with in them (connecting inputs and outputs):
This allows one to reduce details as necessary and change internal implementation of any box without affecting other parts of the system as long as transfer characteristics remain same.
Functions in JS
In programing functions are modeled around the same concepts, even though we messed up an input sets by adding implicit parts that may change over time. In JS function
input set consists of:
- Given arguments.
- Scope bindings.
1. No output
Since functions in JS are first class, they can be part of both input and output sets. Most of the asynchronous APIs take advantage of this fact and require special
callback function argument for continuation passing:
As you can see such functions no longer have have any useful output, which means that they can’t be used for building systems as black boxes. Such functions don’t return values that can be passed over to other boxes.
2. Error handling on each step
I have heard many times people criticizing how in Java exceptions are caught, wrapped and re-thrown again. This reminds me of following:
Basically error propagation in “callback” style APIs is done manually. Note, that in some cases you may want to
try catch actual function body as well.
It easy to end up with two types of functions: synchronous and asynchronous. While it’s possible to make sync function async it’s not the case other way round. This usually means that if one the functions had being converted to be asynchronous all of it’s users will have to be converted as well:
You can’t simply switch to
readFile if becomes necessary.
4. Progress tracking
Finally if your function depends on multiple asynchronous inputs then you will have to manually track each.
Also note that this code assumes that
readURI will call a callback only once, which is not guaranteed.
Describe logic not mechanics
Now consider our last example. Most of the code there is for handling mechanics rather than describing a logic, which feels absolutely very wrong. As a matter of fact actual logic can be expressed as:
So, would not it be better to abstract timing out of logic when it’s not necessary rather than keep solving all this issues in each and every function ? As a mater of fact solution has being there for ages in a form of promises, but for some reason people and web standards tend to use callbacks instead. Maybe because they feel complicated, but that does not necessary has to be the case:
We should not be handling and propagating exceptions manually in each function, we should only handle them when we plan to recover:
If we just want to group multiple values into one there is an
Array for that no need to track progress of each eventual value if we just care about a group!
Noticed a pattern ? We just write a logic, and if it needs to handle asynchronous input we wrap it into
Whats really important here is that such functions can be used to build systems in black boxes, since they do have input and output. Demonstration of that is an example from above (assuming that
readURI returns promise):
Implementing such a solution takes about 100 lines of code (ignoring comments), and that’s more or less what any other control flow library costs anyway. I wish all of you to have more time to concentrate on logic of your program instead of mechanics & small promise library may be a good first step!