Time and Relative Distance in Source (Code)

Reasoning about a program’s behavior is extremely tricky in the best of circumstances. When you throw in asynchronicity, it is the absolute worst. It’s like your code is trapped in a convoluted time travel movie. You want to perform some operation, but that requires stepping into a time portal and coming out at some indeterminate future date. Who knows what has changed since you’ve been gone? Add a few more asynchronous operations, and your code very quickly becomes a tangled mess of wibbly-wobbly, timey-wimey stuff.

There’s no magic bullet to save you from all the complexities of asynchronous code. That said, I’ve found that some approaches are drastically more effective than others. Generally speaking, I think of the approaches of dealing with asynchronicity as falling on a continuum from mind-bending “I’ve gone cross-eyed” code to passing a baton in a relay race.

Here are the four types of “time travel” I’ve used in code, from hardest to easiest:

  1. Asynchronous messages
  2. Callbacks
  3. Promises/Tasks
  4. Async/await

I will walk through each of these using a recent example of some code I wrote for a Xamarin project. Starting off with a difficult-to-use SDK, I wrapped asynchronous messages all the way up to async/await.

Asynchronous Messages

With asynchronous messages, your code initiates a request to some external API. Elsewhere in your code, you must have already created a “listener” that is waiting to hear any messages back from the external API. The onus is on you, the consumer, to determine how the messages you send are related to the messages you receive.

I think of this as traveling through both time and space. You start in one part of the code with a request, and end in an entirely different part of the code with a response. For my “Doctor Who”-loving readers, dealing with this type of code makes me feel like I’ve run afoul of a Weeping Angel.

I’m currently writing some code using a Java Bluetooth API that operates in this mode. So for example, the code might look like this:


// return type is void, nothing to check here
bluetoothApi.sendMessage(new OutboundMessage("will generate a response"));

// earlier, elsewhere in the code ...
blueToothApi.listen(new MessageReceiver() {
    @Override
    public void onMessage(InboundMessage message) {
        // look at message to determine what to do, and good luck correlating with a request
        if (message.wasTryingToDoOneThing()) {
            // ...
        } else if (message.wasTryingToDoAnotherThing()) {
           // ...
        }
    }
});

Sometimes, this is the only way to handle asynchronous code. For example, maybe a message in one direction does not always correlate with one response in the other direction. Nonetheless, I hope it is plain that trying to keep track of the series of events this way is a pain in the neck. Imagine, for instance, trying to set up a sequence of several request/response pairs, where subsequent requests depend on the previous response.

Callbacks

Fortunately, in cases where you can programmatically correlate outbound and inbound messages in the API, you can wrap the message-handling code so that you can use callbacks. Callbacks allow the request and response to stay close to each other in the source code, so at least we’re staying in the same spot when we time travel.

Below is some code that mirrors the logic I wrote to wrap the bidirectional messages. Basically, it just manages to keep a queue of requests. When a response comes in, it finds the matching request and triggers its handler. If the handler is defined in a location that closes over other variables, you can put the response handling next to where the request is sent. It’s this closure aspect that gives the handler its power.


private Vector<Pair<OutboundMessage,MessageHandler>> handlers = new Vector<Pair<OutboundMessage,MessageHandler>>();

public interface IMessageHandler {
    public void handleResponse(InboundMessage message);
}

private boolean messagesMatch(OutboundMessage outbound, InboundMessage inbound) {
    // ...
}

private boolean sendMessage(OutboundMessage message, IMessageHandler handler) {
    handlers.add(handler);
    bluetoothApi.sendMessage(message);
}

blueToothApi.listen(new MessageReceiver() {
    @Override
    public void onMessage(InboundMessage message) {
        Pair<OutboundMessage,MessageHandler> matchingHandler = null;
        for (Pair<OutboundMessage,MessageHandler> pair; handlers) {
            if (messagesMatch(pair.first, message)) {
                matchingHandler = pair;
                break;
            }
        }
        if (matchingHandler != null) {
            handlers.remove(matchingHandler);
            matchingHandler.second.handleResponse(message);
        }
    }
});

// later ...
final String contextVariable = "helpful data";
sendMessage(outboundMessage, new IMessageHandler() {
    @Override
    public void handleResponse(InboundMessage message) {
        // now I know where I came from, and I can access contextVariable etc.
    }
}

That’s a lot better in my book. But it’s still not great. After all, there is a reason someone coined the term “callback hell.” Setting up a sequence of request/response pairs with callbacks can lead to very nested code very quickly:


sendMessage(outboundMessage, new IMessageHandler() {
    @Override
    public void handleResponse(InboundMessage firstResponse) {
        OutboundMessage secondMessage = createSecondMessage(firstResponse);
        sendMessage(secondMessage, new IMessageHandler() {
             @Override
             public void handleResponse(InboundMessage secondResponse) {
                OutboundMessage thirdMessage = createThirdMessage(firstResponse, secondResponse);
                sendMessage(thirdMessage, new IMessageHandler() {
                    @Override
                    public void handleResponse(InboundMessage thirdResponse) {
                        doSomethingWithThirdResponse(response);
                    }
                });
             }        
        });
    }
}

Promises are the way out of this mess.

Promises

As we will see, promises are much nicer than callbacks. Given that my project is in Xamarin, I want to write most of my code in C#. That means using Tasks. Trying to get Android-compatible Java APIs for futures to work in Xamarin C# sounded like a major pain, so I decided to transform from callback to promise in C#. Accordingly, from here on, I will be using C# in the example code.

So how do we go from a callback to a promise in C#? (Or to put it differently, how do you go from an Action<T> to a Task<T>?) Why, a TaskCompletionSource<T>, of course. This handy little class gives you the ability to expose a Task<T> and also to push results or exceptions into the task. Basically, you:

  • Create a TaskCompletionSource<T>
  • Hand its Task off to the code that wants a promise of a T at some point in the future
  • Hand the TaskCompletionSource off to the Action<T> callback that happens when a response comes in

Here’s an example (remember, this is in Xamarin C# interoperating with Java):


public class CallbackToTask : Java.Lang.Object, IMessageHandler 
{
    private TaskCompletionSource<InboundMessage> _tcs = new TaskCompletionSource<InboundMessage>();
    public Task<InboundMessage> Task 
    { 
        get { return _tcs.Task; }
    }
    public void handleResponse(InboundMessage message) {
        _tcs.SetResult(message);
    }
}

Task<InboundMessage> SendRequest(OutboundMessage outboundMessage)
{
    var handler = new CallbackToTask();
    sendMessage(outboundMessage, handler);
    return handler.Task;
}

Tasks are chain-able. You can transform them via additional asynchronous operations without getting into callback hell. That is to say, if you have a Task<T>, you can turn it into a Task<U> not only by providing a function that transforms T to U, but with a function that transforms T to a Task<U>. In other words, multiple asynchronous operations can be “collapsed” into one, and you can chain again from that point.

Let’s see what this means for our previous “callback hell” example:


SendRequest(firstMessage).ContinueWith(t => 
    {
        var initialResponse = t.Result;
        return SendRequest(createSecondMessage(initialResponse));
    }).ContinueWith(t =>
        var secondResponse = t.Result;
        return SendRequest(createThirdMessage(initialResponse, secondResponse));
    }).ContinueWith(t =>
        var thirdResponse = t.Result;
        doSomethingWithThirdResponse(thirdResponse);
    });        

Much better. Tasks are much more powerful than callbacks because they can be passed around, transformed, and combined. You can also synchronously wait on the result. Using Tasks might still involve traveling through time and space, but the Task object essentially represents the ability to move the time portal to a more useful location. You can even use the portal multiple times, from multiple locations!

Async/Await

But there’s one last piece. The async and await keywords are basically just syntactic sugar over Tasks or Promises, but they can make code read almost as if it were taking place sequentially, without time travel–yet you still get all the benefits of asynchronicity. Most notably, using async/await prevents deadlocks by not locking up a thread while waiting for a response.

Let’s take one final look at our “callback hell” example, now with async/await:


var initialResponse = await SendRequest(firstMessage);
var secondResponse = await SendRequest(createSecondMessage(initialResponse));
var thirdResponse = await SendRequest(createThirdMessage(initialResponse, secondResponse));
doSomethingWithThirdResponse(thirdResponse);

Back on our initial callback example, we were six levels deep by the time we got thirdResponse out. With async/await, we aren’t nested at all. If it weren’t for the await keyword, you might think we are dealing with synchronous code.

Conclusion

I’ve added quite a few abstractions to the original messaging API. Extra layers of abstraction are only good insofar as they give you more power and flexibility in implementing your business rules. But I think it is pretty clear that Tasks and async/await are quite powerful abstractions.

Also, for the “type theory curious,” if you understand how I’ve used Tasks and async/await in this blog post, you already have the beginnings of an understanding of functors and monads. But perhaps that’s a topic best saved for another blog post. In the meantime, I’ve got some more asynchronous code to write. Allons-y!