Things to consider when rewriting code to use the new async/await syntax provided by Future::AsyncAwait.

In part 1 we looked at how to rewrite code that previously used plain Future on its own for sequencing operations, into using the neater async/await syntax provided by Future::AsyncAwait. In this part we'll take a look at how to handle the various forms of repeating and iteration provided by Future::Utils::repeat.


repeat as a while loop

Using plain Future the structure of repeating loops such as while loops can be hard to express, and so the Future::Utils module provides a handy utility, repeat, to help write these. Now that we are able to use full async/await syntax within the code, we don't have to use this and instead we can write a regular while loop in regular perl code, with await expressions inside it.

A regular perl while loop has its condition test at the start as compared to the repeat loop testing the condition after the first iteration, so it is often neatest to write it using a while(1) loop and an explicit last to break out of the loop once the condition is satisfied.

# previously
use Future::Utils 'repeat';
sub example {
  repeat {
    FIND_NEXT_THING();
  } while => sub { defined $_[0] };
}

# becomes
async sub example {
  while(1) {
    my $thing = await FIND_NEXT_THING();
    last if !$thing;
  }
}

In a repeat loop, it is common to use the "previous attempt future" passed in as the first argument in order to detect whether this attempt is the first one or not, so as to decide whether to apply a delay time before a subsequent attempt. In this case, using the last loop control of a regular while(1) loop allows you to just skip over such a delay:

# previously
use Future::Utils 'repeat';
sub example {
  return repeat {
    my $prevf = shift;
    ($prevf ? $loop->delay_future(after => 2) : Future->done)
      ->then(sub {
        return TRY_DO_TASK();
      })
  } until => sub { $_[0]->get->is_success };
}

# becomes
async sub example {
  while(1) {
    my $result = await TRY_DO_TASK();
    last if $result->is_success;
    
    await $loop->delay_future(after => 2);
  }
}

repeat as a foreach loop

Another use for repeat is to create an iteration loop over a set of values. As before, we can write this using an await expression inside a regular perl foreach loop.

# previously
use Future::Utils 'repeat';
sub example {
  my @parts = ...;
  return repeat {
    my $idx = shift;
    return PUT_PART($idx, $parts[$idx]);
  } foreach => [0 .. $#parts];
}

# becomes
async sub example {
  my @parts = ...;
  foreach my $idx (0 .. $#parts) {
    await PUT_PART($idx, $parts[$idx]);
  }
}

Sometimes the repeat ... foreach loop is accompanied by an otherwise argument to give some code for what happens when the loop finishes - perhaps it generates some sort of error condition. Plain perl foreach loops don't have an exact equivalent, but in the likely case that the loop is the final piece of code in the function, a return expression can be used to provide the result, and the failure handling code can be put after the loop.

# previously
use Future::Utils 'repeat';
sub example {
  return repeat {
    return TRY_DO_THING();
  } foreach => [1 .. 10],
    while => sub { !$_[0]->get->is_success },
    otherwise => sub {
      return Future->fail("Unable to do the thing in 10 attempts");
    };
}

# becomes
async sub example {
  foreach my $attempt (1 .. 10) {
    my $result = await TRY_DO_THING();
    return $result if $result->is_success;
  }
  
  Future::Exception->throw("Unable to do the thing in 10 attempts");
}

The fmap family of functions

The fmap function and similarly-named functions provide a concurrent-capable version of the map perl operator. Because it takes an extra argument to control the level of concurrency, and as currently await expressions don't work inside perl's map (RT129748), it is best to continue to use fmap and similar where possible.

Don't forget that since fmap returns a Future you can still await on the result, and that the loop body can be an async sub, allowing you to use await expressions inside it.

# previously
use Future::Utils 'fmap1';
use List::Util 'sum';
sub example {
  my @uids = ...;
  return (fmap1 {
    my $uid = shift;
    return GET_USER($uid)->then(sub {
      my ($info) = @_;
      return GET_USER_BALANCE($info);
    });
  } foreach => [@uids], concurrent => 10)->then(sub {
    my @balances = @_;
    return Future->done(sum @balances);
  });
}

# becomes
use Future::Utils 'fmap1';
use List::Util 'sum';
async sub example {
  my @uids = ...;
  my @balances = await fmap1(async sub {
    my $uid = shift;
    my $info = await GET_USER($uid);
    return await GET_USER_BALANCE($info);
  }, foreach => [@uids], concurrent => 10);
  
  return sum @balances;
}

In part 3 we'll finish off by looking at the conditionals and concurrency provided by needs_all or similar.