第三天 - 高阶 Promises

通过组合 Promises 来创建新的复杂 Promise

Mojolicious 7.49 添加了自己的 Promises/A+ 规范实现。莫霍克在第14天写到了这些:你答应召唤! 在 2017 年的 Mojolicious Advent Calender 中,他向你展示了如何同时获取多个网页。这个 Advent 条目扩展了更多的 Promise 技巧。

Promise 是一种旨在消除嵌套回调(也称为“回调地狱”)的结构。正确编写的 Promises 链具有扁平结构,易于线性跟随。

高阶 Promise 是包含其他 Promise 并以其身份为基础的 Promise。 Mojo::Promise::Role::HigherOrder 发行版提供了三个角色,您可以将它们混合到 Mojo::Promise 中以创建包含 Promises 的更好看的 Promise。不过,在你看到它们之前,先看看 Mojo::Promise 已经提供的两个。

All

只有当 all Promise 都解决时,所有承诺才会解决。如果其中一个被拒绝,则拒绝所有承诺。这意味着如果一个被拒绝并且不需要知道任何其他人的状态,整个 Promise 知道该怎么做。

1
2
3
4
5
6
7
8
9
10
11
12
use Mojo::Promise;
use Mojo::UserAgent;
my $ua = Mojo::UserAgent->new;

my @urls = ( ... );
my @all_sites = map { $ua->get_p( $_ ) } @urls;
my $all_promise = Mojo::Promise
->all( @all_sites )
->then(
sub { say "They all worked!" },
sub { say "One of them didn't work!" }
);

然而,Promise 不需要以任何顺序完成他们的工作,所以不要以此为基础。

先到先得

当第一个 Promise 不再等待时,“race”就会解决,之后不需要其他 Promise 继续工作。

1
2
3
4
5
6
7
8
9
10
11
use Mojo::Promise;
use Mojo::UserAgent;
my $ua = Mojo::UserAgent->new;

my @urls = ( ... );
my @all_sites = map { $ua->get_p( $_ ) } @urls;
my $all_promise = Mojo::Promise
->race( @all_sites )
->then(
sub { say "One of them finished!" },
);

Any

“any” 承诺在其第一个承诺结算时立即解决。这与 race 略有不同,因为至少有一个 Promise 必须解决。被拒绝的承诺并没有像 race 那样解决 any 问题。这应该像 bluebirdjs 中的 any 一样。

这是一个程序,它提取配置的 CPAN 镜像并测试它可以获取 index.html 文件。为了确保它找到该文件而不是某个强制门户网站,它会在正文中查找“Jarkko”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
use v5.28;
use utf8;
use strict;
use warnings;
use feature qw(signatures);
no warnings qw(experimental::signatures);

use File::Spec::Functions;
use Mojo::Promise;
use Mojo::Promise::Role::HigherOrder;
use Mojo::UserAgent;
use Mojo::URL;

use lib catfile( $ENV{HOME}, '.cpan' );
my @mirrors = eval {
no warnings qw(once);
my $file = Mojo::URL->new( 'index.html' );
require CPAN::MyConfig;
map { say "1: $_"; $file->clone->base(Mojo::URL->new($_))->to_abs }
$CPAN::Config->{urllist}->@*
};

die "Did not find CPAN::MyConfig\n" unless @mirrors;
my $ua = Mojo::UserAgent->new;

my @all_sites = map {
$ua->get_p( $_ )->then( sub ($tx) {
die unless $tx->result->body =~ /Jarkko/ });
} @mirrors;
my $any_promise = Mojo::Promise
->with_roles( '+Any' )
->any( @all_sites )
->then(
sub { say "At least one of them worked!" },
sub { say "None of them worked!" },
);

$any_promise->wait;

Some

当一定数量的 Promise 解决时,一些 Promise 会解决。您可以指定成功所需的数量,并在该数字解析时解决一些 Promise。这应该像 bluebirdjs 中的 some 一样。

此示例修改以前的程序以查找多个有效的镜像。您可以指定需要解决的问题的数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
use v5.28;
use utf8;
use strict;
use warnings;
use feature qw(signatures);
no warnings qw(experimental::signatures);

use File::Spec::Functions;
use Mojo::Promise;
use Mojo::Promise::Role::HigherOrder;
use Mojo::UserAgent;
use Mojo::URL;

use lib catfile( $ENV{HOME}, '.cpan' );
my @mirrors = eval {
no warnings qw(once);
my $file = Mojo::URL->new( 'index.html' );
require CPAN::MyConfig;
map { say "1: $_"; $file->clone->base(Mojo::URL->new($_))->to_abs }
$CPAN::Config->{urllist}->@*
};

die "Did not find CPAN::MyConfig\n" unless @mirrors;
my $ua = Mojo::UserAgent->new;

my $count = 2;
my @all_sites = map {
$ua->get_p( $_ )->then( sub ($tx) {
die unless $tx->result->body =~ /Jarkko/ });
} @mirrors;
my $some_promise = Mojo::Promise
->with_roles( '+Some' )
->some( \@all_sites, 2 )
->then(
sub { say "At least $count of them worked!" },
sub { say "None of them worked!" },
);

$some_promise->wait;

None

当所有的Promise被拒绝时,“none” Promise 就会解决。这是一个微不足道的案例,可能在某个地方很有用,而且我创建它主要是因为 Perl 6 有一个 none junction(实际上并不是一回事)。有时可能更容易检查没有兑现的承诺,而不是其中一个被拒绝。

对于这个非常简单的示例,请考虑检查没有站点是令人讨厌的“404 File Not Found”的任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
use v5.28;
use utf8;
use strict;
use warnings;
use feature qw(signatures);
no warnings qw(experimental::signatures);

use Mojo::UserAgent;
my $ua = Mojo::UserAgent->new;

use Mojo::Promise;
use Mojo::Promise::Role::HigherOrder;

my @urls = qw(
https://www.learning-perl.com/
https://www.perl.org/
https://perldoc.perl.org/not_there.pod
);

my @all_sites = map {
my $p = $ua->get_p( $_ );
$p->then( sub ( $tx ) {
$tx->res->code == 404 ? $tx->req->url : die $tx->req->url
} );
} @urls;

my $all_promise = Mojo::Promise
->with_roles( '+None' )
->none( @all_sites )
->then(
sub { say "None of them were 404!" },
sub { say "At least one was 404: @_!" },
);

$all_promise->wait;

结论

很容易用较小的 Promise 制作新的 Promise 来代表复杂的情况。您可以将 Mojolicious 为您创造的 Promise 与您自己的手工制作 Promise 相结合,几乎可以做任何您喜欢的事情。