How Perl programmers efficiently manage asynchronous program flows
Wrong Mistakes
But, let me get back to the nested client code: The Pyramid of Doom won't correctly process any errors that might occur. What happens, for example, if the response comes back with an error in the second step? The first-stage callback jumps to its callback on successful completion of the web request and asks the event loop to issue a further request. If this request fails, it is difficult to reach the context of the first stage or even the original program, which actually needs to hear about the problem and print error diagnostics and determine the root cause.
Or, how can the main program discover whether all the results from parallel remote web requests are in place or if a few are still missing? How can you interrupt the program flow, if a request fails, and ignore the requests that follow? You still need control over the program flow, even in asynchronous programming; it is this exact problem that JavaScript programmers have attempted to solve in recent years with several competing approaches, standing on the shoulders of giants who came up with it 30 years ago in functional languages.
PubSub
Software components that rely on sending and receiving events to communicate with one another can knock down the Pyramid of Doom. The approach relies on a publish/subscribe (PubSub) model, in which objects demonstrate their interest in incoming events and, if they receive them, launch methods. They additionally offer methods that allow other objects to send events to them.
The whole thing runs asynchronously, because an event that is sent won't be processed by the target object immediately – only when it finds time to do so. Also, the event emitter does not synchronously wait for a result to be sent back immediately; rather, it returns to its own affairs and, while doing so, watches out for an incoming event that contains the results of the request.
The CPAN Object::Event module offers a simple implementation of this procedure for Perl. A class inherits from Object::Event and then uses the reg_cb()
(for "register callback") method to register a callback for a given event. In addition, this also provides the inherited event()
method, called by external objects that seek to trigger actions in the object of their desire.
Implementing the PubSub model, Listing 3 first defines a WgetPubSub
class whose objects register a callback for the request
event in the constructor. Upon receiving an event, they then retrieve the URL that arrives with the event payload by contacting the web server and then use a new event named response
to return the result once it has been received. For the chained web requests to happen in sequence, line 40 uses reg_cb()
to define a callback for response
. When it gets the result of the first request, it creates another PubSub object, $wget2
, to initiate a new web request.
Listing 3
http-get-pubsub
The code of the main program is not nested, but linear, which improves readability. Also, it is up to the programmer to send individual PubSub objects to other functions, or even third-party packages, for further processing, if this is useful to create a logical division.
PubSub also makes error handling easier. Because the second stage of the web request can easily use the $wget
variable to access the web agent in the first stage, it is just as easy to send a message in case of error, for example, with an event named error
, carrying the error message or code.
If the component is listening for these events, it notices that a problem has occurred downstream and, in turn, can notify a component in the main program.
Empty Promises
Promises [3] are a promising (sic!) new method for asynchronous programming. An asynchronous function with a callback returns an abstract object of the class Promises – a kind of window into the future. It is initially unknown whether the promise will ever receive a result or just cause an error. Until an event happens somewhere else, the promise is a kind of externally controlled, hybrid being, waiting for someone to make a decision, pull a switch, and feed in any resulting data.
The easiest way to explain how a promise works is to look at an example. Line 5 of Listing 4 defines a deferred object – a kind of promise with decision-making power. Line 7 derives a promise from this, and the then()
method assigns two alternative states to the promise: Schrödinger's cat is alive in one case and dead in the other. The two are obviously incompatible; only one can come true at some point.
Listing 4
promise-cat
The decision is made at the moment at which the parent Deferred
object calls one of two methods: If it calls resolve()
, the first state applies, if it calls reject()
, the second state becomes a reality. Once the switch has been pulled, there is no turning back; the status of the promise is then fixed.
The difference between a Deferred and a Promise only relates to accessing the reject()
and resolve()
methods. A Deferred can trigger the methods and thus decide the fate of the promise derived from it. A Promise can only respond; it is not free to decide.
In the case of an asynchronous function that returns a promise, no decision has initially been made on whether you will see a result or an error message. The switch for the decision is pulled by the callback when data arrives or an error occurs. This only happens later on, however, after the current program flow has long passed control back to the event loop. This is why a promise happily takes its instructions from the main program after it has been returned by the asynchronous function; at this point, no results exist yet and no one has had a chance since to call the resolve
or the reject
callbacks.
Listing 5 converts the http_get()
function from the AnyEvent module collection into a function named fetch_url()
in line 10, which expects a URL and returns a Promise. The callback, which http_get()
calls if web data emerges or an error occurs, sets the switch with resolve()
or reject()
.
Listing 5
http-get-promise
Line 26 uses the then()
method to define the callback for a successful web request and leverages the property of Promises as per the latest promise/A+ specification [4], which stipulates that such a callback can then return a Promise. The $prom2
grabs this and enters the third round as of line 32.
The output from Listing 5 is again identical to that of Listings 1 and 3; all three scripts query the same URLs and get the same responses from the server. Because the resolve
or reject
callbacks, and thus the then()
method, in turn return a promise, the chain of requests can even be implemented as follows without temporary variables:
->then( sub { # success } ) ->then( sub { # success } )-> [...]
The Promises module, programmed exactly according to the specification, then makes sure that it processes the whole chain and immediately stops if an error with a reject
call occurs in one link.
In this example, the code is easier to read than in the original, nested callback pyramid. Once again, a modern programming technique from a completely different language has found its way back into Good Old Perl.
Mike Schilli
Mike Schilli works as a software engineer with Yahoo! in Sunnyvale, California. He can be contacted at mailto:mschilli@perlmeister.com. Mike's homepage can be found at http://perlmeister.com.
Infos
- Burnham, Trevor. Async JavaScript: Build More Responsive Apps with Less Code: Pragmatic Express, 2012
- Listings for this article: ftp://linux-magazin.com/pub/listings/magazine/170
- "You're missing the point of promises" by Domenic Denicola: https://blog.domenic.me/youre-missing-the-point-of-promises/
- Promises/A+ specification: https://promisesaplus.com
« Previous 1 2
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
TUXEDO Computers Unveils Linux Laptop Featuring AMD Ryzen CPU
This latest release is the first laptop to include the new CPU from Ryzen and Linux preinstalled.
-
XZ Gets the All-Clear
The back door xz vulnerability has been officially reverted for Fedora 40 and versions 38 and 39 were never affected.
-
Canonical Collaborates with Qualcomm on New Venture
This new joint effort is geared toward bringing Ubuntu and Ubuntu Core to Qualcomm-powered devices.
-
Kodi 21.0 Open-Source Entertainment Hub Released
After a year of development, the award-winning Kodi cross-platform, media center software is now available with many new additions and improvements.
-
Linux Usage Increases in Two Key Areas
If market share is your thing, you'll be happy to know that Linux is on the rise in two areas that, if they keep climbing, could have serious meaning for Linux's future.
-
Vulnerability Discovered in xz Libraries
An urgent alert for Fedora 40 has been posted and users should pay attention.
-
Canonical Bumps LTS Support to 12 years
If you're worried that your Ubuntu LTS release won't be supported long enough to last, Canonical has a surprise for you in the form of 12 years of security coverage.
-
Fedora 40 Beta Released Soon
With the official release of Fedora 40 coming in April, it's almost time to download the beta and see what's new.
-
New Pentesting Distribution to Compete with Kali Linux
SnoopGod is now available for your testing needs
-
Juno Computers Launches Another Linux Laptop
If you're looking for a powerhouse laptop that runs Ubuntu, the Juno Computers Neptune 17 v6 should be on your radar.