Asynchronous Function Decorators

Introduction

tl;dr
Function decorators are a pattern for extracting reusable “ideas” from your code. This document describes how to write a bookend (“before-and-after”) decorator in CoffeeScript that works with callbacks (instead of return values).

Let’s say that you have a critical section of code and you want to use a mutex to lock the code. In CoffeeScript you might write something like this:

criticalSectionA = () ->
  puts "critical section A"
  "Apple"

acquireLock = (name) -> puts "locked #{name}"   # <- e.g. some locking function
releaseLock = (name) -> puts "released #{name}" # <- e.g. some release function

doFooA = () ->
  acquireLock("A")
  value = criticalSectionA()
  releaseLock("A")
  value

Running:

puts doFooA()

Outputs:

locked A
critical section A
released A
Apple

Here we acquire the lock, run our critical section, release the lock, and we have our final value Apple. Simple enough.

Now let’s say you need to lock another section of code:

criticalSectionB = () ->
  puts "critical section B"
  "Banana"

doFooB = () ->
  acquireLock("B")
  value = criticalSectionB()
  releaseLock("B")
  value

Hmm. If we compare doFooB to doFooA we can see that they are definitely doing different things:

  1. We’re acquiring a different lock ("B" vs. "A") and
  2. We’re calling a different function for the critical section (criticalSectionB vs. criticalSectionA)

But despite their differences on the surface, they’re very similar conceptually: every time we acquire a lock we need to release it when we’re finished using it.

Implementing locking this way smells funny in two ways:

  1. Our locking logic is intertwined with our function’s logic.
  2. We have to explicitly store the value of the critical section and remember to return it after we release the lock.

Rephrased in a positive light, we’d like to:

  1. Have the locking logic separate from our function’s core purpose (let’s call it the “business logic“) and
  2. Encapsulate the idea of locking without having to remember the cleanup steps every time we try to use it

Identifying “Aroundness”

Locking isn’t the only situation where we have this pattern. We often need to perform a cleanup step such as closing files, closing sockets, release a resource from a pool, etc.

What we need is a way to specify “aroundness” that we can reuse anytime we encounter the pattern “initialize > do something > cleanup”. Decorators provide a way to do this.

Decorators are functions that take other functions as their arguments and add new behaviors to those functions. The idea is that the function returned from the decorator can masquerade as the original function, but now it has super powers.

A Locking Decorator

Reg Braithwaite has written a wonderful article on decorators in CoffeeScript. In that article, he describes how to write various decorators such as before, after, and around.

Let’s use Reg’s around function to write our locking decorator.

around is a function which takes the decoration (also a function) as its only argument. The decoration itself takes one argument: cb which “represents” our original function (the business logic). The intuition here is that our business logic will be run when we invoke cb.

In our case, we will invoke our setup (i.e. acquiring a lock) before cb and invoke our cleanup (i.e. releasing a lock) after cb.

lockingA = around (cb) ->
  acquireLock("A")
  cb()
  releaseLock("A")

lockingA is now our decorator and we can use lockingA to wrap any function with our locking logic.

We use lockingA by invoking it and passing a base function (our business logic) as the argument.

doFooADecorated = lockingA () ->
  criticalSectionA()

Now let’s run doFooADecorated()

puts doFooADecorated()

This outputs:

locked A
critical section A
released A
Apple

Great! These two functions feel much cleaner than what we had before. We have a clear separation of concerns between the two functions, it’s easy to read, and if we change our locking implementation we need only to change one function.

Clean Up the Decorator

I’m sure you can see one major shortcoming in the previous example: Our decorator lockingA only works if you want to lock "A". Ideally, we’d like to be able to use this same decorator with a lock of any name.

We can do this by wrapping the decorator itself in a function:

locking = (which) ->
  around (cb) ->
    acquireLock(which)
    cb()
    releaseLock(which)

doFooADecorated = locking("A") () -> criticalSectionA()
doFooBDecorated = locking("B") () -> criticalSectionB()

Let’s try running these functions:

puts doFooADecorated()
puts ""
puts doFooBDecorated()

Which outputs:

locked A
critical section A
released A
Apple

locked B
critical section B
released B
Banana

Aroundness in a Callback’s World

There are a few problems with our current implementation of lock that show up when you try to use it with callbacks.

Remember our first attempt at implementing locking?

doFooA = () ->
  acquireLock("A")
  value = criticalSectionA()
  releaseLock("A")
  value

In doFooA we assign the result of criticalSectionA to value and return value at the end of the function. In doFooADecorated even though we’re using the around decorator, under the hood we’re still just storing the return value of our business logic (as you can see in the implementation of around here).

However, when we’re doing async code, we typically don’t return a value. Instead, we invoke a callback function send whatever values we need as arguments to that callback.

Let’s put this in concrete terms to see where we run into trouble. Using callbacks our criticalSectionA would probably look like this:

criticalSectionA = (cb) ->
  puts "critical section A"
  cb "Apple"

If we weren’t aware of how around was implemented, our first instinct for writing criticalSectionA might be to write it like we did last time:

lockA = around (cb) ->
  acquireLock("A")
  cb()
  releaseLock("A")

Now we decorate our function with lockA

doFooADecorated = lockA (cb) ->
  criticalSectionA (result) ->
    cb(result)

And run it:

puts "before lock"
doFooADecorated (result) ->
  puts "unlocked?"
  puts result

This outputs:

before lock
locked A
critical section A
unlocked?
Apple
released A

Well that isn’t quite right. Our lock wasn’t released when we expected. Instead, our lock “leaked out” of doFooADecorated and into the rest of our code.

But the situation gets worse before it gets better…

Beware Next Tick

In node.js we have the concept of process.nextTick which defers execution of the current function (this is very common in node libraries e.g. mikeal/request).

Say, for example, that our critical section calls some code that uses process.nextTick.

criticalSectionNT = (cb) ->
  process.nextTick () ->
    puts "critical section NT"
    cb "Apple"

doFooADecorated = lockA (cb) ->
  criticalSectionNT (result) ->
    cb(result)

doSomething = (done) ->
  puts "before lock"
  doFooADecorated (results) ->
    done(results)

Run it:

doSomething (result) ->
  puts result

Outputs:

before lock
locked A
released A
critical section NT
Apple

Egads! This time our lock didn’t leak out — it was released before we called the critical section!

What’s happening here is that in criticalSectionNT when we call process.nextTick execution on that function is deferred and, as a result, the lock is released before we’ve called our critical section.

Revisiting Aroundness

To fix this, we need to revisit what the concept of “aroundness” means when we’re working with callbacks. Let’s get rid of all the nested functions and look at an async doSomething and write the locking logic directly in the function.

doSomething = (done) ->
  acquireLock("A")
  criticalSectionNT (result) ->
    releaseLock("A")
    done(result)

Run it:

doSomething (result) ->
  puts result

Outputs:

locked A
critical section NT
released A
Apple

It works, but it has all of the shortcomings we’re trying to eliminate in the first place.

Locking with Callbacks

So let’s try to extract the locking logic from the business logic. It’s helpful to think through what we want the interface to be before we start on the implementation.

Ideally we’d like

  1. doSomething to contain only business logic and
  2. The locking decorator to be a simple “declaration” at the beginning of the function.

E.g. something like: doSomething = locking (done) -> ...

To implement this we have to ask the question: what is “aroundness” when you’re working with callbacks anyway? If you look at the previous implementation of doSomething you can see that we are doing two things:

  1. acquireLock at the beginning of doSomething (i.e. before doSomething)
  2. releaseLock right before we invoke the callback done (i.e. after doSomething)

Implementing the before step is straightforward, but understanding the after step is a little trickier.

In an asyncronous context, in order to call something “after” a function we have to change the callback argument and inject our after function into the new callback.

lockingA = (base) ->
  (argv..., origCallback) ->

    callback = (callbackArgv...) =>
      releaseLock("A")
      origCallback.apply(this, callbackArgv)

    acquireLock("A")
    base.apply(this, argv.concat(callback))

In lockingA we accept a base function (our business logic) and we return a new function that will masquerade as base.

The base function also takes arguments, but we don’t know what they are, so we capture them all with argv.... However, since the base function is asyncronous, by convention, the callback is the last parameter in the argument list (and that convention becomes a requirement with our decorator).

So we create a new callback that contains the unlocking logic and replace origCallback with our new callback when we invoke base.

doSomething = lockingA (done) ->
  criticalSectionNT (result) ->
    done(result)

Run it:

doSomething (result) ->
  puts result

Outputs:

locked A
critical section NT
released A
Apple

It works!

The Bookend Decorator

Our decorator is still too specific to locking. We still need to extract the ability to have generic before and after logic.

Looking closely at our current version of lockingA we see that there are only two lines specific to locking (acquireLock/releaseLock)

Thinking about the interface again, we’d like to define lockingA by specifying only the locking logic. This function is going to need to two other functions as its arguments. Something like:

lockingA = bookend \
  -> acquireLock("A"),
  -> releaseLock("A")

Let’s abstract lockingA into a new function bookend by doing the following:

  1. Take before and after as arguments
  2. Call before before we call the base function
  3. Create new callback which calls after before we call the old callback
  4. Substitute the old callback with our new callback

bookend = (before, after) ->
  (base) ->
    (argv..., origCallback) ->
      callback = (callbackArgv...) =>
        after.apply(this)
        origCallback.apply(this, callbackArgv)

      before.apply(this)
      base.apply(this, argv.concat(callback))

Now we can define our decorator lockingA:

lockingA = bookend \
  -> acquireLock("A"),
  -> releaseLock("A")

and create a function which we decorate with lockingA

doSomething = lockingA (done) ->
  criticalSectionNT (result) ->
    done(result)

Run it:

doSomething (result) ->
 puts result

Outputs:

locked A
critical section NT
released A
Apple

Parametrizing the Decorator

There’s one remaining problem with our lockingA decorator: just like last time, it only works for lock "A". We can solve that by simply wrapping our call to bookend in a function itself:

locking = (which) ->
  bookend \
    -> acquireLock(which),
    -> releaseLock(which)

Now we can decorate – with parameters!

doSomethingA = locking("A") (done) ->
  criticalSectionNT (result) ->
    done(result)

Run it:

doSomethingA (result) ->
  puts result

Outputs:

locked A
critical section NT
released A
Apple

Success!

What we’ve written here is extremely useful in maintaining separation of concerns in our code. But there are still two important questions we need to answer:

  • What if we want to decorate something with a before only or an after only?
  • What if our before or after decorations are async themselves?

Callback-based decorations

Let’s throw away our decorators again and start with the obvious way to write our example. Only this time let’s make acquireLockCb and releaseLockCb which are asynchronous versions of our locking/unlocking code.

acquireLockCb = (name, cb) ->
  puts "locked #{name}"
  cb(name)

releaseLockCb = (name, cb) ->
  puts "released #{name}"
  cb()

criticalSectionNT = (cb) ->
  process.nextTick () ->
    puts "critical section NT (w/ cb)"
    cb null, "Apple"

doSomething = (done) ->
  acquireLockCb "A", (lock) ->
    criticalSectionNT (err, result) ->
      releaseLockCb lock, () ->
        done(err, result)

Run it:

doSomething (err, result) ->
  puts result

This outputs:

locked A
critical section NT (w/ cb)
released A
Apple

Now let’s extract before and after, one step at a time.

Extracting Async Before

Before jumping right into a generic before decorator, let’s first write a decorator that does only locking and ignores the unlocking.

lockABeforeCb = (base) ->
  (argv..., origCallback) ->
    acquireLockCb "A", (err, lock) ->
      base.apply(this, argv.concat(origCallback))

doSomething = lockABeforeCb (done) ->
  criticalSectionNT (err, result) ->
    releaseLockCb "A", (releaseErr) ->
      done(err, result)

Run it:

doSomething (err, result) ->
  puts result

Outputs:

locked A
critical section NT (w/ cb)
released A
Apple

Now we need to pull acquireLockCb out into beforeCb.

beforeCb = (before) ->
  (base) ->
    (argv..., origCallback) ->
      callback = (beforeArgv...) =>
        base.apply(this, argv.concat(origCallback))
      before.apply(this, [callback])

lockABeforeCb = beforeCb (cb) ->
    acquireLockCb "A", cb

doSomething = lockABeforeCb (done) ->
  criticalSectionNT (err, result) ->
    releaseLockCb "A", (releaseErr) ->
      done(err, result)

Run it:

doSomething (err, result) ->
  puts result

Outputs:

locked A
critical section NT (w/ cb)
released A
Apple

Extracting Async After

Let’s temporarily discard the before decorator so we can see the after decorator more clearly.

unlockAAfterCb = (base) ->
  (argv..., origCallback) ->
    newCallback = (callbackArgv...) ->
      releaseLockCb "A", (err) ->
        origCallback.apply(this, callbackArgv)
    base.apply(this, argv.concat(newCallback))

doSomething = unlockAAfterCb (done) ->
  acquireLockCb "A", (err, lock) ->
    criticalSectionNT (err, result) ->
      done(err, result)

Run it:

doSomething (err, result) ->
 puts result

Outputs:

locked A
critical section NT (w/ cb)
released A
Apple

Now let’s generalize the unlocking into an after decorator:

afterCb = (after) ->
  (base) ->
    (argv..., origCallback) ->
      newCallback = (callbackArgv...) =>
        after.apply(this, [-> origCallback.apply(this, callbackArgv)])
      base.apply(this, argv.concat(newCallback))

To make this function comprehensible, it might help to read the description of this function out-loud:

  • afterCb is a function which takes a decoration and returns a decorator
  • The decorator is a function which adds functionality to a base function
  • The base function takes any number of arguments with a callback as the last argument
  • A newCallback is created which calls the decoration before it invokes the original callback
  • The base function is invoked with this newCallback

unlockAAfterCb = afterCb (cb) ->
  releaseLockCb "A", cb

doSomething = unlockAAfterCb (done) ->
  acquireLockCb "A", (err, lock) ->
    criticalSectionNT (err, result) ->
      done(err, result)

Run it:

doSomething (err, result) ->
 puts result

Outputs:

locked A
critical section NT (w/ cb)
released A
Apple

A neat feature about these decorators is that we can chain them!

doSomething = lockABeforeCb unlockAAfterCb (done) ->
  criticalSectionNT (err, result) ->
    done(err, result)

A New Bookend Combinator

Because we can chain beforeCb and afterCb, writing a bookend decorator becomes trivial:

bookendcb = (before, after) ->
  (base) -> beforeCb(before) afterCb(after) base

lockACb = bookendcb \
  (cb) -> acquireLockCb("A", cb),
  (cb) -> releaseLockCb("A", cb)

doSomething = lockACb (done) ->
  criticalSectionNT (err, result) ->
    done(err, result)

Summary

We’ve gone from a function with locking logic threaded throughout to creating a higher-order decorator we can use to capture reusable ideas.

Reg has added async.before, async.after, and async.provided to method-combinators. Try using method-combinators in your next project and let me know how it goes!

References

Thanks to Reg Braithwaite, Devin Foley, Moises Beltran, and Rob Zinkov for reading drafts of this.

If you enjoyed this article then you should follow me on twitter where I discuss higher-order software design, bioluminescence, and beekeeping.

Share:
  • del.icio.us
  • Reddit
  • Technorati
  • Twitter
  • Facebook
  • Google Bookmarks
  • HackerNews
  • PDF
  • RSS
This entry was posted in programming. Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.
  • Sandy Place

    This is an awesome article. Well done.