1Promises::Cookbook::GenUtsleerInCtornot(r3i)buted Perl DPorcoummiesnetsa:t:iCoonokbook::GentleIntro(3)
2
3
4
6 Promises::Cookbook::GentleIntro - All you need to know about Promises
7
9 version 1.04
10
12 If you have ever done any async programming, you will be familiar with
13 "callback hell", where one callback calls another, calls another, calls
14 another... Promises give us back a top-to-bottom coding style, making
15 async code easier to manage and understand. It looks like synchronous
16 code, but execution is asynchronous.
17
18 The Promises module is event loop agnostic - it can be used with any
19 event loop. Backends exist for AnyEvent (and thus all the event loops
20 supported by AnyEvent) and Mojo::IOLoop. But more of this later in
21 "Integration with event loops".
22
23 There are two moving parts:
24
25 Deferred objects
26 Deferred objects provide the interface to a specific async request.
27 They execute some asynchronous action and return a promise.
28
29 Promise objects
30 A promise is like a placeholder for a future result. The promise
31 will either be resolved in case of success, or rejected in case of
32 failure. Promises can be chained together, and each step in the
33 chain is executed sequentially.
34
35 The easiest way to understand how Deferred and Promise objects work is
36 by example.
37
38 Deferred objects
39 A deferred object is used to signal the success or failure of some
40 async action which can be implemented in the async library of your
41 choice. For instance:
42
43 use Promises qw(deferred);
44 use AnyEvent::HTTP qw(http_get);
45 use JSON qw(decode_json);
46
47 sub fetch_it {
48 my ($uri) = @_;
49 my $deferred = deferred;
50 http_get $uri => sub {
51 my ($body, $headers) = @_;
52 $headers->{Status} == 200
53 ? $deferred->resolve( decode_json($body) )
54 : $deferred->reject( $headers->{Reason} )
55 };
56 $deferred->promise;
57 }
58
59 The above code makes an asynchronous "http_get" request to the
60 specified $uri. The result of the request at the time the subroutine
61 returns is like Schrödinger's cat: both dead and alive. In the future
62 it may succeed or it may fail.
63
64 This sub creates a Promises::Deferred object using "deferred", which is
65 either:
66
67 • resolved on success, in which case it returns the request "body",
68 or
69
70 • rejected on failure, in which case it returns the reason for
71 failure.
72
73 As a final step, the deferred object returns a Promises::Promise object
74 which represents the future result.
75
76 That's all there is to know about Promises::Deferred.
77
78 Promise objects
79 Promises are a lot like "try"/"catch"/"finally" blocks except that they
80 can be chained together. The most important part of a promise is the
81 then() method:
82
83 $promise->then(
84 sub { success! },
85 sub { failure }
86 );
87
88 The then() method takes two arguments: a success callback and a failure
89 callback. But the important part is that it returns a new promise,
90 which is the thing that allows promises to be chained together.
91
92 The simple genius of promises (and I can say that because I didn't
93 invent them) will not be immediately obvious, but bear with me.
94 Promises are very simple, as long as you understand the execution flow:
95
96 Resolving or rejecting a Promise
97
98 use Promises qw(deferred);
99
100 my $deferred = deferred;
101 $deferred->promise->then(
102 sub { say "OK! We received: ".shift(@_)}, # on resolve
103 sub { say "Bah! We failed with: ". shift(@_)} # on reject
104 );
105
106 What this code does depends on what happens to the $deferred object:
107
108 $deferred->resolve('Yay!');
109 # prints: "OK! We received: Yay!"
110
111 $deferred->reject('Pooh!');
112 # prints "Bah! We failed with: Pooh!"
113
114 A Deferred object can only be resolved or rejected once. Once it is
115 resolved or rejected, it informs all its promises of the outcome.
116
117 Chaining resolve callbacks
118
119 As mentioned earlier, the then() method returns a new promise which
120 will be resolved or rejected in turn. Each "resolve" callback will
121 receive the return value of the previous "resolve" callback:
122
123 deferred
124 ->resolve('red','green')
125 ->promise
126
127 ->then(sub {
128 # @_ contains ('red','green')
129 return ('foo','bar');
130 })
131
132 ->then(sub {
133 # @_ contains ('foo,bar');
134 return 10;
135 })
136
137 ->then( sub {
138 # @_ contains (10)
139 });
140
141 All of these example callbacks have just returned a simple value (or
142 values), so execution has moved from one callback to the next.
143
144 Chaining reject callbacks
145
146 Note that in the above example, in each call to then() we specified
147 only a resolved callback, not a rejected callback. If a promise is
148 resolved or rejected, the action gets passed down the chain until it
149 finds a resolved or rejected handler. This means that errors can be
150 handled in the appropriate place in the chain:
151
152 my $deferred = deferred;
153
154 $deferred->promise
155 ->then(
156 sub {
157 my $count = shift();
158 say "Count: $count";
159 return $count+1;
160 }
161 )
162 ->then(
163 sub {
164 my $count = shift();
165 say "Count: $count";
166 return $count+1;
167 }
168 )->then(
169 sub {
170 my $count = shift();
171 say "Final count: $count";
172 return $count+1;
173 },
174 sub {
175 my $reason = shift;
176 warn "Failed to count: $reason"
177 }
178 );
179
180 If the $deferred object is resolved, it will call each resolved
181 callback in turn:
182
183 $deferred->resolve(5);
184 # prints:
185 # Count: 5
186 # Count: 6
187 # Final count: 7
188
189 If the $deferred object is rejected, however, it will skip all of the
190 steps in the chain until it hits the first rejected callback:
191
192 $deferred->reject('Poor example');
193 # warns:
194 # "Failed to count: Poor example"
195
196 Important: Event loops do not like fatal exceptions! For this reason
197 the resolved and rejected callbacks are run in "eval" blocks.
198 Exceptions thrown in either type of callback are passed down the chain
199 to the next rejected handler. If there are no more rejected handlers,
200 then the error is silently swallowed.
201
202 Throwing and handling exceptions
203
204 While you can signal success or failure by calling resolve() or
205 reject() on the $deferred object, you can also signal success or
206 failure in each step of the promises chain.
207
208 • Resolved callbacks are like "try" blocks: they can either execute
209 some code successfully or throw an exception.
210
211 • Rejected callbacks are like "catch" blocks: they can either handle
212 the exception or rethrow it.
213
214 $deferred = deferred;
215
216 $deferred->promise
217 ->then(
218 sub {
219 my $count = shift;
220 die "Count too high!" if $count > 100;
221 return $count
222 }
223 )->then(
224 sub {
225 say "The count is OK. Continuing";
226 return @_
227 },
228 sub {
229 my $error = shift;
230 warn "We have a problem: $error";
231 die $error;
232 }
233 )->then(
234 undef, # no resolved handler
235 sub { return 1; }
236 )-> then(
237 sub {
238 my $count = shift;
239 say "Got count: $count";
240 }
241 )
242
243 There are a few ways this code can execute. We can resolve the
244 $deferred object with a reasonable count:
245
246 $deferred->resolve(5);
247 # prints:
248 # The count is OK. Continuing
249 # Got count: 5
250
251 $defer
252
253 If we reject the $deferred object, the first rejected handler is
254 called. It warns, then rethrows the exception with "die" which calls
255 the next rejected handler. This handler resolves the exception (that
256 is, it doesn't call "die") and returns a value which gets passed to the
257 next resolved handler:
258
259 $deferred->reject('For example purposes')
260 # warns:
261 # We have a problem: For example purposes
262 # prints:
263 # Got count: 1
264
265 Finally, if we resolve the $deferred object with a too large count, the
266 first resolved handler throws an exception, which calls the next
267 rejected handler:
268
269 $deferred->resolve(1000);
270 # warns:
271 # We have a problem: Count too high!
272 # prints:
273 # Got count: 1
274
275 catch()
276
277 In the above example, we called then() with "undef" instead of a
278 resolved callback. This could be rewritten to look a bit cleaner using
279 the catch() method, which takes just a rejected callback.
280
281 # these two lines are equivalent:
282 $promise->then( undef, sub { rejected cb} )
283 $promise->catch( sub { rejected cb } )
284
285 finally()
286
287 Any "try"/"catch" implementation has a "finally" block, which can be
288 used to clean up resources regardless of whether the code in the "try"
289 block succeeded or failed. Promises offer this functionality too.
290
291 The finally() method accepts a single callback which is called
292 regardless of whether the previous step was resolved or rejected. The
293 return value (or any exception thrown in the callback) are thrown away,
294 and the chain continues as if it were not there:
295
296 $deferred = deferred;
297 $deferred->promise
298 ->then(
299 sub {
300 my $count = shift;
301 if ($count > 10) { die "Count too high"}
302 return $count
303 }
304 )->finally(
305 sub { say "Finally got: ".shift(@_) }
306 )->then(
307 sub { say "OK: ". shift(@_) },
308 sub { say "Bah!: ". shift(@_) }
309 );
310
311 If we resolve the $deferred object with a good count, we see:
312
313 $d->resolve(5);
314 # prints:
315 # Finally got: 5
316 # OK: 5
317
318 With a high count we get:
319
320 $d->resolve(20);
321 # prints:
322 # Finally got: Count to high
323 # Bah: 20
324
325 Chaining async callbacks
326
327 This is where the magic starts: each resolved/rejected handler can not
328 only return a value (or values), it can also return a new Promise.
329 Remember that a Promise represents a future value, which means that
330 execution of the chain will stop until the new Promise has been either
331 resolved or rejected!
332
333 For instance, we could write the following code using the fetch_it()
334 sub (see "Deferred objects") which returns a promise:
335
336 fetch_it('http://domain.com/user/123')
337 ->then(
338 sub {
339 my $user = shift;
340 say "User name: ".$user->{name};
341 say "Fetching total comments";
342 return fetch_id($user->{total_comments_url});
343 }
344 )->then(
345 sub {
346 my $total = shift;
347 say "User has left $total comments"
348 }
349 )
350 ->catch(
351 sub {
352 warn @_
353 }
354 );
355
356 This code sends an asynchronous request to get the page for user 123
357 and returns a promise. Once the promise is resolved, it sends an
358 asynchronous request to get the total comments for that user and again
359 returns a promise. Once the second promise is resolved, it prints out
360 the total number of comments. If either promise were to be rejected, it
361 would skip down the chain looking for the first rejected handler and
362 execute that.
363
364 This is organised to look like synchronous code. Each step is executed
365 sequentially, it is easy to read and easy to understand, but it works
366 asynchronously. While we are waiting for a response from "domain.com"
367 (while our promise remains unfulfilled), the event loop can happily
368 continue running code elsewhere in the application.
369
370 In fact, it's not just Promises::Promise objects that can be returned,
371 it can be any object that is ``thenable'' (ie it has a then() method).
372 So if you want to integrate your Promises code with a library which is
373 using Future objects, you should be able to do it.
374
375 Running async requests in parallel
376
377 Sometimes order doesn't matter: perhaps we want to retrieve several web
378 pages at the same time. For that we can use the "collect" helper:
379
380 use Promises qw(collect);
381
382 collect(
383 fetch_it('http://rest.api.example.com/-/product/12345'),
384 fetch_it('http://rest.api.example.com/-/product/suggestions?for_sku=12345'),
385 fetch_it('http://rest.api.example.com/-/product/reviews?for_sku=12345'),
386 )->then(
387 sub {
388 my ($product, $suggestions, $reviews) = @_;
389 # do something with these values
390 },
391 sub { warn @_ }
392 );
393
394 collect() accepts a list of promises and returns a new promise (which
395 we'll call $p for clarification purposes. When all of its promises
396 have been resolved, it resolves $p with the values returned by every
397 promise, in the same order as they were passed in to collect().
398
399 Note: Each promise can return multiple values, so $product,
400 $suggestions and $reviews in the example above will all be array refs.
401
402 If any of the passed in promises is rejected, then $p will also be
403 rejected with the reason for the failure. $p can only be rejected
404 once, so we wil only find out about the first failure.
405
406 Integration with event loops
407 In order to run asynchronous code, you need to run some event loop.
408 That can be as simple as using "CONDITION VARIABLES" in AnyEvent to run
409 the event loop just until a particular condition is met:
410
411 use AnyEvent;
412
413 my $cv = AnyEvent->condvar;
414 collect(
415 fetch_it('http://rest.api.example.com/-/product/12345'),
416 fetch_it('http://rest.api.example.com/-/product/suggestions?for_sku=12345'),
417 fetch_it('http://rest.api.example.com/-/product/reviews?for_sku=12345'),
418 )->then(
419 sub {
420 my ($product, $suggestions, $reviews) = @_;
421 $cv->send({
422 product => $product->[0],
423 suggestions => $suggestions->[0],
424 reviews => $reviews->[0],
425 })
426 },
427 sub { $cv->croak( 'ERROR' ) }
428 );
429
430 # wait for $cv->send or $cv->croak
431 my $results = $cv->recv;
432
433 More usually though, a whole application is intended to be
434 asynchronous, in which case the event loop just runs continuously.
435 Normally you would only need to use $cv's or the equivalent at the
436 point where your application uses a specific async library, as
437 explained in "Deferred objects". The rest of your code can deal purely
438 with Promises.
439
440 Event loop specific backends
441
442 The resolved and rejected callbacks should be run by the event loop,
443 rather than having one callback call the next, which calls the next
444 etc.
445
446 In other words, if a promise is resolved, it doesn't call the resolved
447 callback directly. Instead it adds it to the event loop's queue, then
448 returns immediately. The next time the event loop checks its queue,
449 it'll find the callback in the queue and will call it.
450
451 By default, Promises is event loop agnostic, which means that it
452 doesn't know which event loop to use and so each callback ends up
453 calling the next, etc. If you're writing Promises-based modules for
454 CPAN, then your code should also be event loop agnostic, in which case
455 you want to use Promises like this:
456
457 use Promises qw(deferred collect);
458
459 However, if you are an end user, then you should specify which event
460 loop you are using at the start of your application:
461
462 use Promises backend => ['AnyEvent']; # or "EV" or "Mojo"
463
464 You only need to specify the backend once - any code in the application
465 which uses Promises will automatically use the specified backend.
466
467 Recursing safely with with done()
468 One of the cool things about working with promises is that the return
469 value gets passed down the chain as if the code were synchronous.
470 However that is not always what we want.
471
472 Imagine that we want to process every line in a file, which could be
473 millions of lines. We don't care about the results from each line, all
474 we care about is whether the whole file was processed successfully, or
475 whether something failed.
476
477 In sync code we'd write something like this:
478
479 sub process_file {
480 my $fh = shift;
481 while (my $line = <$fh>) {
482 process_line($line)
483 || die "Failed"
484 }
485 }
486
487 Now imagine that process_line() runs asynchronously and returns a
488 promise. By the time it returns, it probably hasn't executed anything
489 yet. We can't go ahead and read the next line of the file otherwise we
490 could generate a billion promises before any of them has had time to
491 execute.
492
493 Instead, we need to wait for process_line() to complete and only then
494 move on to reading the next line. We could do this as follows:
495
496 # WARNING: EXAMPLE OF INCORRECT CODE #
497
498 use Promises qw(deferred);
499
500 sub process_file {
501 my $fh = shift;
502 my $deferred = deferred;
503 my $processor = sub {
504 my $line = <$fh>;
505 unless (defined $line) {
506 # we're done
507 return $deferred->resolve;
508 }
509 process_line($line)->then(
510
511 # on success, call $processor again
512 __SUB__,
513
514 # on failure:
515 sub {
516 return $deferred->reject("Failed")
517 }
518 )
519 }
520
521 # start the loop
522 $processor->();
523
524 return $deferred->promise
525 }
526
527 This code has two stack problems. The first is that, every time we
528 process a line, we recurse into the current "__SUB__" from the current
529 sub. This problem is solved by specifying one of the "Event loop
530 specific backends" somewhere in our application, which we discussed
531 above.
532
533 The second problem is that every time we recurse into the current
534 "__SUB__" we're waiting for the return value. Other languages use the
535 Tail Call optimization <http://en.wikipedia.org/wiki/Tail_call> to keep
536 the return stack flat, but we don't have this option.
537
538 Instead, we have the done() method which, like then(), accepts a
539 resolved callback and a rejected callback. But it differs from then()
540 in two ways:
541
542 • It doesn't return a promise, which means that the chain ends with
543 the done() step.
544
545 • Callbacks are not run in an "eval" block, so calling die() will
546 throw a fatal exception. (Most event loops, however will catch the
547 exception, warn, and continue running.)
548
549 The code can be rewritten using done() instead of then() and an event
550 loop specific backend, and it will happily process millions of lines
551 without memory leaks or stack oveflows:
552
553 use Promises backend => ['EV'], 'deferred';
554
555 sub process_file {
556 my $fh = shift;
557 my $deferred = deferred;
558 my $processor = sub {
559 my $line = <$fh>;
560 unless (defined $line) {
561 # we're done
562 return $deferred->resolve;
563 }
564 #### USE done() TO END THE CHAIN ####
565 process_line($line)->done(
566
567 # on success, call $processor again
568 __SUB__,
569
570 # on failure:
571 sub {
572 return $deferred->reject("Failed")
573 }
574 )
575 }
576
577 # start the loop
578 $processor->();
579
580 return $deferred->promise
581 }
582
584 Stevan Little <stevan.little@iinteractive.com>
585
587 This software is copyright (c) 2020, 2019, 2017, 2014, 2012 by Infinity
588 Interactive, Inc.
589
590 This is free software; you can redistribute it and/or modify it under
591 the same terms as the Perl 5 programming language system itself.
592
593
594
595perl v5.36.0 2023-01-20Promises::Cookbook::GentleIntro(3)