Back in the old days of programming, the days we used to have blocking function calls, things were easy:
f() gets completed do
g(), and when
g() completes do
h(), and so on. The premise of this blog post is to investigate whether it is possible to still preserve the dependency between functions by still allowing the programmer to syntatically structure the program sequentially.
Ok, enough talk. Let's start with working on a small sequential program snippet.
Simple and intuitive. You just grab the logic behind at first sight:
Um... Not as intiutive as its sequential counterpart. And the nested callback chains expands the code to the right, which makes it harder to understand as well. But, not that bad... with a serious glitch: Orphan exceptions. That is, for instance, who is supposed to catch a
connection error exception after
db.open completes gracefully and the execution passes over the try-catch block? While code will be get polluted a little bit, this problem can be tackled by returning an error, instead of raising an exception.
Better now, at least in terms of correctness.
So far, we always beforehand knew the callbacks that will be nested into each other. That is, we knew that a simple query will follow just after the database connection gets established. What if we wouldn't? What if the next callback is to be dynamically determined according to a runtime variable? Think about this scenario: You need to query the database multiple times depending on the input passed by the user. A pretty common day-to-day practice. Terrorizing the code with unknown number of nested callbacks would buy us no credits.
On the other hand, forming nested callbacks using a recursive function solves the problem.
I admit that this is not intuitive, also more error-prone. (I also could not be sure if I wrote it right. But anyway, you get the idea.) There must be some other way. Wouldn't it be cute if there would exist some sort of
SequentialExecutor helps you to push your functions into an array and executes them in order for you. Specifically, it passes you the pointer to the next function (i.e.,
next) that is supposed to be executed after current function. So, it is up to you to execute it or not. Here is an example using this cute
Yes, now we have something! Let's also try to implement the case where the total number of queries are dynamically determined on the run.
Oops! That is not what we were expecting. Database connection is supposed to be closed at the end of the execution flow. Hrm... Can't we enhance
SequentialExecutor to label tasks with priorities? Here is the poor man's sequential executor with priority support.
Let's give our new gear,
PrioritizedSequentialExecutor, a try.
Mission accomplished! Now we have a fully-fledged sequencer where we can dynamically push tasks with different priorities.
Note that while
PrioritizedSequentialExecutor is quite good at doing what it is advertised for, especially compared to the lines of code written, there exists other libraries (e.g., seq, chainsaw, futures, async, windjs, streamlinejs, etc.) with similar flow-control purposes. While you are at it, you might want to check them out too.