Wait the light to fall

第三天 - 高阶 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 知道该怎么做。

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 继续工作。

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”:

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 一样。

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

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 就会解决。这是一个微不足道的案例,可能在某个地方很有用,而且我创建它主要是因为 Raku 有一个 none junction(实际上并不是一回事)。有时可能更容易检查没有兑现的承诺,而不是其中一个被拒绝。

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

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 相结合,几乎可以做任何您喜欢的事情。