BAM Weblog

Escaping Callback Hell with ClojureScript macros

Brian McKenna2011-07-24

JavaScript needs macros. I've thought this for a long time. This week Rich Hickey announced and released ClojureScript, a version of Clojure that compiles to JavaScript! I'm going to try and use ClojureScript's macro system to overcome one of my JavaScript frustrations.

I like the fact that JavaScript is highly event based but code can easily become callback hell. Take this for example:

app.get('/index', function(req, res, next) {
    User.get(req.params.userId, function(err, user) {
        if(err) next(err);
        db.find({user: user.name}, function(err, cursor) {
            if(err) next(err);
            cursor.toArray(function(err, items) {
                if(err) next(err);
                res.send(items);
            });
        });
    });
});

Yuck! Let's try to fix this with a simpler example. Here are the goals:

  • Write a sequence of function calls
  • Have each call be wrapped with an asynchronous timeout callback

In the end, I want to be able to write code like this:

(timeout/timeout-macro
   (pn "Hello")
   (pn "World")
   (p "Hello")
   (p " ")
   (p "world")
   (p "!")
   (p "!")
   (pn "!")
   (pn (+ 1 2 3))
   (pn "Done"))

Lisps are great at solving this problem because they are homoiconic. They can have powerful macro systems because code can be transformed exactly the same as data. Here is the macro I wrote for this problem:

(defmacro timeout-macro [& body]
  (reduce
   (fn [x y]
     `(timer/callOnce (fn [] ~x ~y) 1000))
   `(fn [])
   (reverse body)))

With the macro, ClojureScript transforms the function calls above into some awesomely ugly JavaScript:

callOnce.call(null, (function () {
  callOnce.call(null, (function () {
    callOnce.call(null, (function () {
      callOnce.call(null, (function () {
        callOnce.call(null, (function () {
          callOnce.call(null, (function () {
            callOnce.call(null, (function () {
              callOnce.call(null, (function () {
                callOnce.call(null, (function () {
                  callOnce.call(null, (function () {
                    return pn.call(null, "Done");
                  }), 1000);
                  return pn.call(null, cljs.core._PLUS_.call(null, 1, 2, 3));
                }), 1000);
                return pn.call(null, "!");
              }), 1000);
              return p.call(null, "!");
            }), 1000);
            return p.call(null, "!");
          }), 1000);
          return p.call(null, "world");
        }), 1000);
        return p.call(null, " ");
      }), 1000);
      return p.call(null, "Hello");
    }), 1000);
    return pn.call(null, "World");
  }), 1000);
  return pn.call(null, "Hello");
}), 1000);

This is known as an automatic continuation-passing style (CPS) transform. I've put the demo up here and posted the code up on Bitbucket.

Please enable JavaScript to view the comments powered by Disqus.