NAME

Promises::Cookbook::SynopsisBreakdown - A breakdown of the SYNOPSIS section of Promises

VERSION

version 1.04

SYNOPSIS

use AnyEvent::HTTP;
use JSON::XS qw[ decode_json ];
use Promises qw[ collect deferred ];

sub fetch_it {
    my ($uri) = @_;
    my $d = deferred;
    http_get $uri => sub {
        my ($body, $headers) = @_;
        $headers->{Status} == 200
            ? $d->resolve( decode_json( $body ) )
            : $d->reject( $body )
    };
    $d->promise;
}

my $cv = AnyEvent->condvar;

collect(
    fetch_it('http://rest.api.example.com/-/product/12345'),
    fetch_it('http://rest.api.example.com/-/product/suggestions?for_sku=12345'),
    fetch_it('http://rest.api.example.com/-/product/reviews?for_sku=12345'),
)->then(
    sub {
        my ($product, $suggestions, $reviews) = @_;
        $cv->send({
            product     => $product,
            suggestions => $suggestions,
            reviews     => $reviews,
        })
    },
    sub { $cv->croak( 'ERROR' ) }
);

my $all_product_info = $cv->recv;

DESCRIPTION

The example in the synopsis actually demonstrates a number of the features of this module, this section will break down each part and explain them in order.

sub fetch_it {
    my ($uri) = @_;
    my $d = deferred;
    http_get $uri => sub {
        my ($body, $headers) = @_;
        $headers->{Status} == 200
            ? $d->resolve( decode_json( $body ) )
            : $d->reject( $body )
    };
    $d->promise;
}

First is the fetch_it function, the pattern within this function is the typical way in which you might wrap an async function call of some kind. The first thing we do it to create an instance of Promises::Deferred using the deferred function, this is the class which does the majority of the work or managing callbacks and the like. Then within the callback for our async function, we will call methods on the Promises::Deferred instance. In the case we first check the response headers to see if the request was a success, if so, then we call the resolve method and pass the decoded JSON to it. If the request failed, we then call the reject method and send back the data from the body. Finally we call the promise method and return the promise 'handle' for this deferred instance.

At this point out asynchronous operation will typically be in progress, but control has been returned to the rest of our program. Now, before we dive into the rest of the example, lets take a quick detour to look at what promises do. Take the following code for example:

my $p = fetch_it('http://rest.api.example.com/-/user/bob@example.com');

At this point, our async operation is running, but we have not yet given it anything to do when the callback is fired. We will get to that shortly, but first lets look at what information we can get from the promise.

$p->status;

Calling the status method will return a string representing the status of the promise. This will be either in progress, resolved, resolving (meaning it is in the process of resolving), rejected or rejecting (meaning it is in the process of rejecting). (NOTE: these are also constants on the Promises::Deferred class, IN_PROGRESS, RESOLVED, REJECTED, etc., but they are also available as predicate methods in both the Promises::Deferred class and proxied in the Promises::Promise class). At this point, this method call is likely to return in progress. Next is the result method:

$p->result;

which will give us back the values that are passed to either resolve or reject on the associated Promises::Deferred instance.

Now, one thing to keep in mind before we go any further is that our promise is really just a thin proxy over the associated Promises::Deferred instance, it stores no state itself, and when these methods are called on it, it simply forwards the call to the associated Promises::Deferred instance (which, as I said before, is where all the work is done).

So, now, lets actually do something with this promise. So as I said above the goal of the Promise pattern is to reduce the callback spaghetti that is often created with writing async code. This does not mean that we have no callbacks at all, we still need to have some kind of callback, the difference is all in how those callbacks are managed and how we can more easily go about providing some level of sequencing and control.

That all said, lets register a callback with our promise.

$p->then(
    sub {
        my ($user) = @_;
        do_something_with_a_user( $user );
    },
    sub {
        my ($err) = @_;
        warn "An error was received : $err";
    }
);

As you can see, we use the then method (again, keep in mind this is just proxying to the associated Promises::Deferred instance) and passed it two callbacks, the first is for the success case (if resolve has been called on our associated Promises::Deferred instance) and the second is the error case (if reject has been called on our associated Promises::Deferred instance). Both of these callbacks will receive the arguments that were passed to resolve or reject as their only arguments, as you might have guessed, these values are the same values you would get if you called result on the promise (assuming the async operation was completed).

It should be noted that the error callback is optional. If it is not specified then errors will be silently eaten (similar to a try block that has not catch). If there is a chain of promises however, the error will continue to bubble to the last promise in the chain and if there is an error callback there, it will be called. This allows you to concentrate error handling in the places where it makes the most sense, and ignore it where it doesn't make sense. As I alluded to above, this is very similar to nested try/catch blocks.

And really, that's all there is to it. You can continue to call then on a promise and it will continue to accumulate callbacks, which will be executed in FIFO order once a call is made to either resolve or reject on the associated Promises::Deferred instance. And in fact, it will even work after the async operation is complete. Meaning that if you call then and the async operation is already completed, your callback will be executed immediately.

So, now lets get back to our original example. I will briefly explain my usage of the AnyEvent condvar, but I encourage you to review the docs for AnyEvent yourself if my explanation is not enough.

So, the idea behind my usage of the condvar is to provide a merge-point in my code at which point I want all the asynchronous operations to converge, after which I can resume normal synchronous programming (if I so choose). It provides a kind of a transaction wrapper if you will, around my async operations. So, first step is to actually create that condvar.

my $cv = AnyEvent->condvar;

Next, we jump back into the land of Promises. Now I am breaking apart the calling of collect and the subsequent chained then call here to help keep things in digestible chunks, but also to illustrate that collect just returns a promise (as you might have guessed anyway).

my $p = collect(
    fetch_it('http://rest.api.example.com/-/product/12345'),
    fetch_it('http://rest.api.example.com/-/product/suggestions?for_sku=12345'),
    fetch_it('http://rest.api.example.com/-/product/reviews?for_sku=12345'),
);

So, what is going on here is that we want to be able to run multiple async operations in parallel, but we need to wait for all of them to complete before we can move on, and collect gives us that ability. As we know from above, fetch_it is returning a promise, so obviously collect takes an array of promises as its parameters. As we said before collect also returns a promise, which is just a handle on a Promises::Deferred instance it created to watch and handle the multiple promises you passed it. Okay, so now lets move onto adding callbacks to our promise that collect returned to us.

$p->then(
    sub {
        my ($product, $suggestions, $reviews) = @_;
        $cv->send({
            product     => $product,
            suggestions => $suggestions,
            reviews     => $reviews,
        })
    },
    sub { $cv->croak( 'ERROR' ) }
);

So, you will notice that, as before, we provide a success and an error callback, but you might notice one slight difference in the success callback. It is actually being passed multiple arguments, these are the results of the three fetch_it calls passed into collect, and yes, they are passed to the callback in the same order you passed them into collect. So from here we jump back into the world of condvars, and we call the send method and pass it our newly assembled set of collected product info. As I said above, condvars are a way of wrapping your async operations into a transaction like block, when code execution encounters a recv, such as in our next line of code:

my $all_product_info = $cv->recv;

the event loop will block until a corresponding send is called on the condvar. While you are not required to pass arguments to send it will accept them and the will in turn be the return values of the corresponding recv, which makes for an incredibly convenient means of passing data around your asynchronous program.

It is also worth noting the usage of the croak method on the condvar in the error callback. This is the preferred way of dealing with exceptions in AnyEvent because it will actually cause the exception to be thrown from recv and not somewhere deep within a callback.

And that is all of it, once recv returns, our program will go back to normal synchronous operation and we can do whatever it is we like with $all_product_info.

AUTHOR

Stevan Little <stevan.little@iinteractive.com>

COPYRIGHT AND LICENSE

This software is copyright (c) 2020, 2019, 2017, 2014, 2012 by Infinity Interactive, Inc.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.