Rewriting Code with Async Await Syntax - Perl
Things to consider when rewriting code to use the new async/await
syntax provided by Future::AsyncAwait
.
The Future
class provides basic asynchronous future functionality, and also a number of handy building-blocks for building larger code structures into meaningful programs. With the addition of the async/await
syntax provided by Future::AsyncAwait
, several of these code structures are no longer necessary, because equivalent code can be written in a style much closer to traditional "straight-line" synchronous Perl code. In addition, other benefits of using Future::AsyncAwait
and futures mean that it is often desirable to rewrite code to using these new forms where possible. In this series of articles I aim to cover many of the situations that will be encountered when performing such a rewrite, and suggest how code may be changed to make use of the new syntax while preserving the existing behaviour.
For this first part, we'll look at uses of Future for sequencing multiple different code blocks to happen one after the other.
->then
chaining
Probably the most common (and among the simplest) examples of future-based code is the use of the ->then
method to schedule further blocks of code to run once the first as completed. These can be rewritten into using the await
keyword to wait for the first expression inside an async sub
, and then simply putting more code afterwards. If there are more ->then
chains following that, then more await
expressions can be used.
# previously
sub example {
return FIRST()->then( sub {
code in the middle;
SECOND();
})->then( sub {
more code goes here;
THIRD();
});
}
# becomes
async sub example {
await FIRST();
code in the middle;
await SECOND();
more code goes here;
await THIRD();
}
Then chaining in async code
Of particular note is to remember to await
even in the final expression of the function, where the call to THIRD()
is expected to return a future instance. If this isn't done then callers of example
will receive a future which will complete as soon as SECOND()
finishes, and its result will itself be the future instance that THIRD()
returned - a double-nested future. The caller likely didn't want that result.
As well as being neater and more readable, containing less of the "machinery noise" from the ->then
method calls, the fact that all of the expressions now appear within the same lexical scope of the body of the function allows them to use the same lexical variables. This means you no longer need to "hoist" variable declarations out of the chain if you need to capture values in one block to be used in a subsequent one.
# previously
sub example {
my $thing;
GET_THING()->then(sub {
($thing) = @_;
do_something_else();
})->then(sub {
USE_THING($thing);
});
}
# becomes
async sub example {
my $thing = await GET_THING();
await do_something_else();
await USE_THING($thing);
}
Async code for ->then chaining
->else
chaining
As future instances can complete with either a successful or a failed result, many examples will use the ->else
method to attach some error-handling code. When using async/await
syntax, a failed future causes an exception to be thrown from the await
keyword, so we can intercept that using the try/catch
syntax provided by Syntax::Keyword::Try
.
(It is important to use Syntax::Keyword::Try
rather than Try::Tiny
, as the latter implements its syntax using anonymous inner functions, which confuse the async/await
suspend-and-resume mechanism, whereas the former uses inline code blocks as part of the containing function, allowing async/await
to work correctly).
A common use of ->else
is to handle a failure by yielding some sort of other "default" value, in place of the value that the failing function would have returned.
# previously
sub example {
return TRY_A_THING()->else(sub {
my ($message) = @_;
warn "Failed to do the thing - $message";
return Future->done($DEFAULT);
});
}
# becomes
use Syntax::Keyword::Try;
async sub {
try {
return await TRY_A_THING();
} catch {
my $message = $@;
warn "Failed to do the thing - $message";
return $DEFAULT;
}
}
Example of Async code
Note here that return
is able to cause the entire containing function to return that result. Sometimes, there would be more code after the ->else
block - perhaps chained by another ->then
. In this case we have to capture the result of the try/catch
into a variable, for later inspection.
# previously
sub example {
return TRY_GET_RESOURCE()->else(sub {
return Future->done($DEFAULT_RESOURCE);
})->then(sub {
my ($resource) = @_;
return USE_RESOURCE($resource);
});
}
# becomes
use Syntax::Keyword::Try;
async sub example {
my $resource;
try {
$resource = await TRY_GET_RESOURCE();
} catch {
$resource = $DEFAULT_RESOURCE;
}
await USE_RESOURCE($resource);
}
Example of Async code
Another use for ->else
is to capture and re-throw a failure, but annotating it somehow to provide more detail. We can handle this by capturing the exception as before, inspecting the details out of it, and rethrowing another one. We can use Future::Exception->throw
to throw an exception containing the additional details from a failed future:
# previously
sub example {
my ($user) = @_;
return HTTP_GET("https://example.org/info/$user")->else(sub {
my ($message, $category, @details) = @_;
return Future->fail(
"Unable to get user info for $user - $message",
$category, @details
);
});
}
# becomes
use Syntax::Keyword::Try;
async sub example {
my ($user) = @_;
try {
return await HTTP_GET("https://example.org/info/$user");
} catch {
my $e = $@;
my $message = $e->message;
Future::Exception->throw(
"Unable to get user info for $user - $message",
$e->category, $e->details
);
}
An example of else code in async code
->transform
The ->transform
method provides a convenience for converting the result into a different form, by use of a code block giving a transformation function. Because an await
operator can appear anywhere within an expression, the relevant code can just be written more directly:
# previously
sub example {
my ($uid) = @_;
return GET_USER_INFO($uid)->transform(
done => sub {
return { uid => $uid, info => $_[0] };
},
);
}
# becomes
async sub example {
my ($uid) = @_;
return { uid => $uid, info => await GET_USER_INFO($uid) };
}
An example of transform code for async await
Immediate Return Values
Sometimes a function doesn't actually perform any asynchronous work, but simply returns a value in an immediate future using Future->done
, perhaps for API or consistency reasons. In this case because an async sub
always returns its value by a Future, we can simply turn the function into an async sub
and return
the value directly - Future::AsyncAwait
will handle the rest:
# previously
sub example {
return Future->done(\%GLOBAL_SETTINGS);
}
# becomes
async sub example {
return \%GLOBAL_SETTINGS;
}
Sub example for async await
In this case, just because we have an async sub
there is no requirement to actually call await
anywhere within it. If we don't, the function will run synchronously and yield its result in an immediate future.
Similarly, sometimes a function will want to return an immediate failure by using Future->fail
. Any exception thrown in the body of an async sub
is turned into a failed future, but if we want to apply the category name and additional details, we have to use Future::Exception->throw
, as already seen above.
# previously
sub example {
my ($req) = @_;
return Future->fail("Invalid user ID", req => $req)
unless is_valid($req->{uid});
...
}
# becomes
async sub example {
my ($req) = @_;
Future::Exception->throw("Invalid user ID", req => $req)
unless is_valid($req->{uid});
...
}
An example of future fail in async await
In part 2 we'll take a look at the various iteration forms provided by Future::Utils::repeat
, and in part 3 we'll finish off by looking at the conditionals and concurrency provided by needs_all
or similar.
Note: This post has been ported from https://tech.binary.com/ (our old tech blog). Author of this post is https://metacpan.org/author/PEVANS