Overview of a new syntax module for managing Future-based code

Introduction

At Binary, we have an increasing amount of code that uses Futures to represent asynchronous operations. The power of these objects comes with the increased complexity of writing readable code that makes use of them. Various articles I have previously written have described Futures and their use. In this article, I want to present a new syntax module that greatly improves the expressive power and neatness of writing Future-based code. This module is Future::AsyncAwait (CPAN link).

The new syntax provided by this module is based on two keywords, async and await that between them provide a powerful new ability to write code that uses Future objects. The await keyword causes the containing function to pause while it waits for completion of a future, and the async keyword decorates a function definition to allow this to happen. These keywords encapsulate the idea of suspending some running code that is waiting on a future to complete, and resuming it again at some later time once a result is ready.

use Future::AsyncAwait;

async sub get_price {
    my ($product) = @_;

    my $catalog = await get_catalog();

    return $catalog->{$product}->{price};
}

This already reads a little neater than how this might look with a ->then chain:

sub get_price {
    my ($product) = @_;

    return get_catalog()->then(sub {
        my ($catalog) = @_;

        return Future->done($catalog->{$product}->{price});
    });
}

This new syntax makes a much greater impact when we consider code structures like foreach loops:

use Future::AsyncAwait;

async sub send_message {
    my ($message) = @_;

    foreach my $chunk ($message->chunks) {
        await send_chunk($chunk);
    }
}

Previously we'd have had to use Future::Utils::repeat to create the loop:

use Future::Utils qw( repeat );

sub send_message {
    my ($message) = @_;

    repeat {
        my ($chunk) = @_;
        send_chunk($chunk);
    } foreach => [ $message->chunks ];
}

Because the entire function is suspended and resumed again later on, the values of lexical variables are preserved for use later on:

use Future::AsyncAwait;

async sub echo {
    my $message = await receive_message();
    await delay(0.2);
    send_message($message);
}

If instead we were to do this using ->then chaining, we'd find that we either have to hoist a variable out to the main body of the function to store $message, or use a further level of nesting and indentation to make the lexical visible to later code:

sub echo {
    my $message;
    receive_message()->then(sub {
        ($message) = @_;
        delay(0.2);
    })->then(sub {
        send_message($message);
    });
}

# or

sub echo {
    receive_message()->then(sub {
        my ($message) = @_;
        delay(0.2)->then(sub {
            send_message($message);
        });
    });
}

These final examples are each equivalent to the version using async and await above, yet are both much longer, and more full of the lower-level "machinery" of solving the problem, which obscures the logical flow of what the code is trying to achieve.

Comparison With Other Languages

This syntax isn't unique to Perl - a number of other languages have introduced very similar features.

ES6, aka JavaScript (link):

async function asyncCall() {
  console.log('calling');
  var result = await resolveAfter2Seconds();
  console.log(result);
}

Python 3 (link):

async def main():
    print('hello')
    await asyncio.sleep(1)
    print('world')

C# (link):

public async Task<int> GetDotNetCountAsync()
{
    var html = await
        _httpClient.GetStringAsync("https://dotnetfoundation.org");

    return Regex.Matches(html, @"\.NET").Count;
}

Dart (link):

main() async {
  var context = querySelector("canvas").context2D;
  var running = true;    // Set false to stop game.

  while (running) {
    var time = await window.animationFrame;
    context.clearRect(0, 0, 500, 500);
    context.fillRect(time % 450, 20, 50, 50);
  }
}

In fact, much like the recognisable shapes of things like if blocks and while loops, it is starting to look like the async/await syntax is turning into a standard language feature across many languages.

Current State

At the time of writing, this module stands at version 0.21, and has been the result of an intense round of bug-fixing and improvement over the Christmas and New Year break. While it isn't fully production-tested and ready for all uses yet, we are starting to experiment with using it in a number of less production-critical code paths (such as unit or integration testing) in order to help shake out any further bugs that may arise, and generally evaluate how far and how soon we want to push it further in to Binary code.

This version already handles a lot of even non-trivial cases, such as in conjunction with the try/catch syntax provided by Syntax::Keyword::Try:

use Future::AsyncAwait;
use Syntax::Keyword::Try;

async sub copy_data
{
    my ($source, $destination) = @_;

    my @rows = await $source->get_data;

    my $successful = 0;
    my $failed     = 0;

    foreach my $row (@rows) {
        try {
            await $destination->put_row($row);
            $successful++;
        } catch {
            $log->warn("Unable to handle row ID %s: %s",
                $row->{id}, $@);
            $failed++;
        }
    }

    $log->info("Copied %d rows successfully, with %d failures",
        $successful, $failed);
}

In part 2, we will look some details of the implementation, a few known bugs, and explore what next for the module and how best to make use of it at Binary.