To Be Continued: Async Simplified
December 02, 2012 · Clojure, ClojureScript, Node.js
Asynchronous programming can greatly boost the performance of I/O-bound applications. This wins it many proponents and is a major contributor to the popularity of Node.js.
In Clojure, where we can bring macros into play, we can do better. The remainder of this post introduces an asynchronous Clojure/ClojureScript library that I have been working on called To Be Continued. Its goal is to make it easier to write, read, and maintain asynchronous Clojure and ClojureScript programs.
Callback placeholders, indicated by
... at the end of a form, are the central feature of To Be Continued. When one is encountered during the expansion of TBC’s macros, it is replaced with a generated callback function that arranges for the flow of execution to continue where it left off once the result is available.
In addition to telling the TBC macros that a given form requires a callback, the
... serves as a hint to readers that an asynchronous call is taking place behind the scenes even though TBC is allowing the flow of the code to be expressed uninterrupted.
Callback placeholders are used with TBC’s threading macros and parallel binding form, described below.
To Be Continued provides two asynchronous-aware threading macros,
-+->>, analogous to clojure.core’s
->> macros, respectively. They expect a value or an expression returning a value as their first argument, a callback form to invoke with the final result as their last argument, and any number of intermediate forms in between.
The intermediate forms may be synchronous function invocations, asynchronous function invocations having a callback placeholder as their final argument, or a combination of the two.
The return value of TBC threading macros is always nil. It is expected that the final form will be a callback that will do something useful with the result passed to it.
(use 'to-be-continued.core) (defn async-square "Invoke the callback with the square of x after 1 second" [x callback] (.start (Thread. (fn  (Thread/sleep 1000) (callback (* x x)))))) (-+-> 7 (async-square ...) (str " monkeys") println) ;; => nil ;; 49 monkeys
Once the asynchronous result of
async-square has been supplied to its automatically-generated callback, processing resumes with a synchronous call to
str and then to
println which serves as the final callback and displays the result.
let-par macro is the asynchronous equivalent of Clojure’s
let macro. It allows the results of multiple asynchronous functions, executed in parallel, to be bound to variables that can be referenced the body expression.
(defn async-sum-of-squares [x y callback] (let-par [x-sq (async-square x ...) y-sq (async-square y ...)] (-+->> (+ x-sq y-sq) (str "The answer is: ") callback))) (async-sum-of-squares 2 5 println) ;; => nil ;; The answer is 29
Note that the use of the
-+->> macro above is not strictly necessary because there are no asynchronous forms before the final callback. Its use is encouraged, however, for error handling purposes. Once error handling support has been incorporated into TBC (see Status and Next Steps, below) any errors that occur in the chain will be properly handled and propagated.
-+->>, the value of a
let-par expression is always nil. It is expected to invoke a callback with its result.
Example: ClojureScript + Node.js
To see To Be Continued in action, please check out tbc-node-example. It is an example project using TBC with ClojureScript and Node.js to asynchronously fetch data from GitHub’s API.
It has example usages of the macros described above as well
map-par, a parallel, asynchronous equivalent of
Status and Next Steps
TBC currently lacks support for handling errors that occur during asynchronous computation. Therefore, it is not yet suitable for production use. Error handling is my next focus and will be available in the 0.2.0 release.
In the mean time, the existing functionality should be sufficient to get a feel for the library. Feedback is very welcome, whether as comments here or as issues filed on its GitHub project.blog comments powered by Disqus