(Update: a library based on this series is in use in SamePlace and is available here.)
Here’s something most languages can do which JavaScript can’t (at least when hosted in the browser):
print("Going to bed...");
sleep(3000);
print("Sigh, I only slept three seconds.")
In JavaScript that would be:
dump("Going to bed...\n");
setTimeout(function() {
dump("Sigh, I only slept three seconds.\n");
}, 3000);
sleep()
blocks until it’s done. That wouldn’t work very well in JavaScript because, while sleep()
is sleeping, buttons, menus, and so on would stop responding. Same goes for sending AJAX requests and waiting for responses, or for querying the user and waiting for input.
So, JavaScript has to resort to callbacks. It isn’t that bad when you just want to wait three seconds, but imagine sending an AJAX request, waiting for the result, asking the user something based on that result, sending another AJAX request… callback depending on callback depending on callback. Or asynchrospaghetti, for friends.
Anyway, I’ll show a way to get in JavaScript something that looks like the first example. It’ll take a lot of effort, which sucks because the outcome is so trivial, but it’ll also lay foundations to build cool stuff, e.g. message passing.
You’ll need a JavaScript implementation that supports generators, which means JavaScript 1.7, which means no Internet Explorer. But then again if you’re reading this blog you’re not too heart-broken about that.
A basic form of pseudo-blocking call
Last things first. This is what we’ll end up with:
var driver = proc(function() {
dump("Going to bed...\n");
yield(sleep(3000));
dump("Sigh, I only slept three seconds.\n")
});
driver();
Aside from yield()
, the body of the function looks refreshingly similar to the first example. We’ll explode sleep()
immediately so that we don’t have to keep track of extra indirection. As for proc()
, we’ll talk about it in the next part, but if you’re eager to see something running, scroll to the bottom to find its implementation.
var driver = proc(function() {
dump("Going to bed...\n");
var pseudoBlockingCall = function(driver) {
window.setTimeout(driver, 3000);
});
yield(pseudoBlockingCall);
dump("Sigh, I only slept three seconds.\n")
});
driver();
Let’s take this apart. There are three actors here:
- a process constructor (the
proc()
call) - a process driver, defined in
proc()
and called from various places) - the process definition itself (the argument to
proc()
)
The process constructor proc()
receives the process definition as an argument, internally sets up a process driver for it, then returns the process driver. Invoking the process driver starts the process.
The process definition passed to proc()
looks like a function but it isn’t—it’s a generator. Think of a generator as a function that can be called, it suspends, returns a value, it’s called again, it resumes, it suspends again, returns another value, and so on. Those intermediate returns happen when a yield()
instruction is met. We’re going to exploit this to simulate blocking calls.
In the next part, we’ll unroll execution line by line. For now, here’s the implementation of proc()
:
function proc(processDef) { // Execute the argument, resulting in a generator object. The // process isn't executed yet. var process = processDef(); // The process driver. It's deceptively simple! This will be // returned from proc() and passed as argument to the pseudo-blocking // calls, and will be invoked many times. function driver() { var pseudoBlockingCall = process.next(); pseudoBlockingCall(driver); } return driver; }
Articles in this series:
- Part 1: Problem and basic solution
- Part 2: Execution dissected
- Part 3: Returning values
- Part 4: Error handling