50Ply Blog

Building Things

Asynchronous Sequential Code Shape

| Comments

I’ve been writing more asynchronous code lately and I’m annoyed by the shape that asynchronous sequential code takes. For example:

1
2
3
4
5
6
7
8
9
fetch("/foo.json", function(text) {
   var url = text + ".html";
   fetch(url, function(result) {
      view.show(result);
      timeout(1000, function() {
          view.makeEditable();
      });
   });
});

Sure, it’s not that bad–but there’s a lot of noise in that code that’s more about it being asynchronous than it is about the actual task I’m trying to accomplish. If I was instead writing code that would run in a thread that could block freely I’d write something like:

1
2
3
4
5
6
var text = fetch("/foo.json");
var url = text + ".html";
var result = fetch(url);
view.show(result);
sleep(1000);
view.makeEditable();

But, now I’m doing potentially terrible things like updating my view from an arbitrary thread. Also, there better be a mutex hidden somewhere to protect any global state (like view.) However, regardless of the newfound dangers, the intent of this code is much clearer.

So, my threaded example is clearer but my async example is safer. Wouldn’t it be nice to have the best of both worlds? We can!

I’ve been writing more code in Clojurescript recently. Since Clojurescript has turbo-powered-lisp-like macros, we can just extend the language to support a better “shape” for asynchronous sequential code. Macros are great at transforming the shape of your code.

First, here’s what the very first example would look like translated into Clojurescript but without our shape enhancing macro:

1
2
3
4
5
6
7
8
9
(fetch "/foo.json"
  (fn [text]
    (let [url (str text ".html")]
      (fetch url
        (fn [result]
          (.show view result)
          (timeout 1000
            (fn []
              (.makeEditable view))))))))

Now, with our shape-shifting macro:

1
2
3
4
5
6
7
(doasync
  [text [fetch "/foo/json"]
   url (str text ".html")
   result [fetch url]
   _ (.show view result)
   _ [timeout 1000]
   _ (.makeEditable view)])

Looks a lot like our threaded case, right? It turns out that the code that the doasync macro generates has exactly the same effect as our other asynchronous examples. But, thanks to doasync, we get to express that asynchronous code in the more clear shape that, until now, only our threaded example had been able to achieve.

doasync is part of my MOVE example on GitHub.

Comments