50Ply Blog

Building Things

Defasync

| Comments

Christopher’s comments to yesterday’s post forced me to think more about the return value of the doasync form and whether it would make sense to give it a body.

doasync doesn’t return anything useful. How could it? doasync is designed for callback driven code that’s probably ultimately pumped by the event loop. So, when it is time to return from the form we probably haven’t actually done anything yet.

But, as I was using doasync to write interactors, I noticed that all of my doasync forms really did have a “return” value that they were passing on to their own callback (nextfn in the linked code.) This sounds like a pattern to capture in a new macro.

defasync works like a defn but for defining asynchronous functions. I define an “asynchronous function” to be a function that takes a callback as its last argument and “returns” its ultimate value by passing it to the callback exactly once. Since my definition includes a “return” value we finally have a reasonable place to put whatever the body computes.

Here’s how my asynchronous function definitions looked before:

1
2
3
4
5
6
(defn get-current-list [state nextfn]
  "[async] get the current list from the server and update the model"
  (doasync
   [current-list [get-json "/resources/current-list.json"]
    _ (models/set-current-list state current-list)
    _ (nextfn current-list)]))

And here’s how they look now:

1
2
3
4
5
6
(defasync get-current-list [state]
  "[async] get the current list from the server and update the model"
  [current-list [get-json "/resources/current-list.json"]]

  (models/set-current-list state current-list)
  current-list)

I’m trying to mimic defn as much as possible with defasync. Like defn, defasync has a name and a set of arguments. After an optional docstring, defasync accepts a vector containing the same asynchronous sequential binding style as doasync. This is the only part of the code where the asynchronous invocation form [func arg1 arg2] is allowed.

Next comes the body. The body is executed once all of the bindings are established. The return value of the body is passed to our now implicit nextfn. Did you notice that nextfn is missing? It’s inserted into the last slot in the argument list by defasync and only invoked once with the result of executing the body.

Here’s another example (again from interactors.) Here we create a dialog asking the user for input and then collect the result when it becomes available. In the body we update our model with the user’s new data.

1
2
3
4
5
6
7
(defasync create-new-todo [state view]
  "[async] create a new item in the current list"
  [list (models/current-list state)
   input-dialog (views/make-input-dialog "What do you want to do?")
   input [events/register-once [:ok-clicked input-dialog]]]

  (models/add-todo state list input))

Comments