1Promises::Cookbook::SynUospesrisCBornetarkidbouwtne(d3P)PreormlisDeosc:u:mCeonotkabtoiookn::SynopsisBreakdown(3)
2
3
4
6 Promises::Cookbook::SynopsisBreakdown - A breakdown of the SYNOPSIS
7 section of Promises
8
10 version 1.04
11
13 use AnyEvent::HTTP;
14 use JSON::XS qw[ decode_json ];
15 use Promises qw[ collect deferred ];
16
17 sub fetch_it {
18 my ($uri) = @_;
19 my $d = deferred;
20 http_get $uri => sub {
21 my ($body, $headers) = @_;
22 $headers->{Status} == 200
23 ? $d->resolve( decode_json( $body ) )
24 : $d->reject( $body )
25 };
26 $d->promise;
27 }
28
29 my $cv = AnyEvent->condvar;
30
31 collect(
32 fetch_it('http://rest.api.example.com/-/product/12345'),
33 fetch_it('http://rest.api.example.com/-/product/suggestions?for_sku=12345'),
34 fetch_it('http://rest.api.example.com/-/product/reviews?for_sku=12345'),
35 )->then(
36 sub {
37 my ($product, $suggestions, $reviews) = @_;
38 $cv->send({
39 product => $product,
40 suggestions => $suggestions,
41 reviews => $reviews,
42 })
43 },
44 sub { $cv->croak( 'ERROR' ) }
45 );
46
47 my $all_product_info = $cv->recv;
48
50 The example in the synopsis actually demonstrates a number of the
51 features of this module, this section will break down each part and
52 explain them in order.
53
54 sub fetch_it {
55 my ($uri) = @_;
56 my $d = deferred;
57 http_get $uri => sub {
58 my ($body, $headers) = @_;
59 $headers->{Status} == 200
60 ? $d->resolve( decode_json( $body ) )
61 : $d->reject( $body )
62 };
63 $d->promise;
64 }
65
66 First is the "fetch_it" function, the pattern within this function is
67 the typical way in which you might wrap an async function call of some
68 kind. The first thing we do it to create an instance of
69 Promises::Deferred using the "deferred" function, this is the class
70 which does the majority of the work or managing callbacks and the like.
71 Then within the callback for our async function, we will call methods
72 on the Promises::Deferred instance. In the case we first check the
73 response headers to see if the request was a success, if so, then we
74 call the "resolve" method and pass the decoded JSON to it. If the
75 request failed, we then call the "reject" method and send back the data
76 from the body. Finally we call the "promise" method and return the
77 promise 'handle' for this deferred instance.
78
79 At this point out asynchronous operation will typically be in progress,
80 but control has been returned to the rest of our program. Now, before
81 we dive into the rest of the example, lets take a quick detour to look
82 at what promises do. Take the following code for example:
83
84 my $p = fetch_it('http://rest.api.example.com/-/user/bob@example.com');
85
86 At this point, our async operation is running, but we have not yet
87 given it anything to do when the callback is fired. We will get to that
88 shortly, but first lets look at what information we can get from the
89 promise.
90
91 $p->status;
92
93 Calling the "status" method will return a string representing the
94 status of the promise. This will be either in progress, resolved,
95 resolving (meaning it is in the process of resolving), rejected or
96 rejecting (meaning it is in the process of rejecting). (NOTE: these
97 are also constants on the Promises::Deferred class, "IN_PROGRESS",
98 "RESOLVED", "REJECTED", etc., but they are also available as predicate
99 methods in both the Promises::Deferred class and proxied in the
100 Promises::Promise class). At this point, this method call is likely to
101 return in progress. Next is the "result" method:
102
103 $p->result;
104
105 which will give us back the values that are passed to either "resolve"
106 or "reject" on the associated Promises::Deferred instance.
107
108 Now, one thing to keep in mind before we go any further is that our
109 promise is really just a thin proxy over the associated
110 Promises::Deferred instance, it stores no state itself, and when these
111 methods are called on it, it simply forwards the call to the associated
112 Promises::Deferred instance (which, as I said before, is where all the
113 work is done).
114
115 So, now, lets actually do something with this promise. So as I said
116 above the goal of the Promise pattern is to reduce the callback
117 spaghetti that is often created with writing async code. This does not
118 mean that we have no callbacks at all, we still need to have some kind
119 of callback, the difference is all in how those callbacks are managed
120 and how we can more easily go about providing some level of sequencing
121 and control.
122
123 That all said, lets register a callback with our promise.
124
125 $p->then(
126 sub {
127 my ($user) = @_;
128 do_something_with_a_user( $user );
129 },
130 sub {
131 my ($err) = @_;
132 warn "An error was received : $err";
133 }
134 );
135
136 As you can see, we use the "then" method (again, keep in mind this is
137 just proxying to the associated Promises::Deferred instance) and passed
138 it two callbacks, the first is for the success case (if "resolve" has
139 been called on our associated Promises::Deferred instance) and the
140 second is the error case (if "reject" has been called on our associated
141 Promises::Deferred instance). Both of these callbacks will receive the
142 arguments that were passed to "resolve" or "reject" as their only
143 arguments, as you might have guessed, these values are the same values
144 you would get if you called "result" on the promise (assuming the async
145 operation was completed).
146
147 It should be noted that the error callback is optional. If it is not
148 specified then errors will be silently eaten (similar to a "try" block
149 that has not "catch"). If there is a chain of promises however, the
150 error will continue to bubble to the last promise in the chain and if
151 there is an error callback there, it will be called. This allows you to
152 concentrate error handling in the places where it makes the most sense,
153 and ignore it where it doesn't make sense. As I alluded to above, this
154 is very similar to nested "try/catch" blocks.
155
156 And really, that's all there is to it. You can continue to call "then"
157 on a promise and it will continue to accumulate callbacks, which will
158 be executed in FIFO order once a call is made to either "resolve" or
159 "reject" on the associated Promises::Deferred instance. And in fact, it
160 will even work after the async operation is complete. Meaning that if
161 you call "then" and the async operation is already completed, your
162 callback will be executed immediately.
163
164 So, now lets get back to our original example. I will briefly explain
165 my usage of the AnyEvent "condvar", but I encourage you to review the
166 docs for AnyEvent yourself if my explanation is not enough.
167
168 So, the idea behind my usage of the "condvar" is to provide a merge-
169 point in my code at which point I want all the asynchronous operations
170 to converge, after which I can resume normal synchronous programming
171 (if I so choose). It provides a kind of a transaction wrapper if you
172 will, around my async operations. So, first step is to actually create
173 that "condvar".
174
175 my $cv = AnyEvent->condvar;
176
177 Next, we jump back into the land of Promises. Now I am breaking apart
178 the calling of "collect" and the subsequent chained "then" call here to
179 help keep things in digestible chunks, but also to illustrate that
180 "collect" just returns a promise (as you might have guessed anyway).
181
182 my $p = collect(
183 fetch_it('http://rest.api.example.com/-/product/12345'),
184 fetch_it('http://rest.api.example.com/-/product/suggestions?for_sku=12345'),
185 fetch_it('http://rest.api.example.com/-/product/reviews?for_sku=12345'),
186 );
187
188 So, what is going on here is that we want to be able to run multiple
189 async operations in parallel, but we need to wait for all of them to
190 complete before we can move on, and "collect" gives us that ability.
191 As we know from above, "fetch_it" is returning a promise, so obviously
192 "collect" takes an array of promises as its parameters. As we said
193 before "collect" also returns a promise, which is just a handle on a
194 "Promises::Deferred" instance it created to watch and handle the
195 multiple promises you passed it. Okay, so now lets move onto adding
196 callbacks to our promise that "collect" returned to us.
197
198 $p->then(
199 sub {
200 my ($product, $suggestions, $reviews) = @_;
201 $cv->send({
202 product => $product,
203 suggestions => $suggestions,
204 reviews => $reviews,
205 })
206 },
207 sub { $cv->croak( 'ERROR' ) }
208 );
209
210 So, you will notice that, as before, we provide a success and an error
211 callback, but you might notice one slight difference in the success
212 callback. It is actually being passed multiple arguments, these are the
213 results of the three "fetch_it" calls passed into "collect", and yes,
214 they are passed to the callback in the same order you passed them into
215 "collect". So from here we jump back into the world of "condvars", and
216 we call the "send" method and pass it our newly assembled set of
217 collected product info. As I said above, "condvars" are a way of
218 wrapping your async operations into a transaction like block, when code
219 execution encounters a "recv", such as in our next line of code:
220
221 my $all_product_info = $cv->recv;
222
223 the event loop will block until a corresponding "send" is called on the
224 "condvar". While you are not required to pass arguments to "send" it
225 will accept them and the will in turn be the return values of the
226 corresponding "recv", which makes for an incredibly convenient means of
227 passing data around your asynchronous program.
228
229 It is also worth noting the usage of the "croak" method on the
230 "condvar" in the error callback. This is the preferred way of dealing
231 with exceptions in AnyEvent because it will actually cause the
232 exception to be thrown from "recv" and not somewhere deep within a
233 callback.
234
235 And that is all of it, once "recv" returns, our program will go back to
236 normal synchronous operation and we can do whatever it is we like with
237 $all_product_info.
238
240 Stevan Little <stevan.little@iinteractive.com>
241
243 This software is copyright (c) 2020, 2019, 2017, 2014, 2012 by Infinity
244 Interactive, Inc.
245
246 This is free software; you can redistribute it and/or modify it under
247 the same terms as the Perl 5 programming language system itself.
248
249
250
251perl v5.38.0 2023-P0r7o-m2i1ses::Cookbook::SynopsisBreakdown(3)