Rand Stats

Future

cpan:HANENKAMP

NAME

Future - A futuristic extension to Promises and other awaitables

SYNOPSIS

use Future;

# Works like Promise.start({})
my $f = Future.start: { long-running-process() };
my $result = await $f;

# Or from a Promise.new
my $p = Promise.new;
my $f = Future.from-promise($p);
$p.keep(42);
my $result = await $f;

# But you can add types
my Future[Int] $f .= start: { long-running-process() };
my $result = await $f; # guaranteed to be an Int or throws exception
CATCH { when X::Future::Mismatch { .say } }

# And nested Futures automatically unravel nested Futures/Promises
my $f = Future.start: {
    start { 42 }
}
say await $f; # prints 42, not Promise.new(...)

# Callback chains have entirely different semantics from Promise
# - catch() - allows you to handle exceptions
# - then() - allows for asynchronous processing
my Future[Int] $i = Future.start(
    { open('file.txt', :r) }
).catch(
    -> X::IO { open('alt.txt', :r) }
).catch(
    -> X::IO { "42" }
).then(
    -> Str $val { $val.Numeric },
    -> IO::Handle $fh { $fh.slurp.Numeric },
).constrain(Int);

say await $i;

DESCRIPTION

EXPERIMENTAL. This is an experimental API. Suggestions welcome.

Promises are wonderful, but having become accustomed to some of the features of promises in other languages, I found a few things lacking. I wanted to make something that made Promises a little nicer to work with. So to the Future.

A Future is just a placeholder for a future value. It does not directly provide any means of resolving that value. Instead, it depends on something else to eventually provide that value:

This means a Future can get its value from basically anything, including a Promise, Supply, or Channel.

A Future provides asynchronous callbacks similar to those of Promise. These will be called when the future is fulfilled or rejected. This is different from a Promise, whose callbacks are only called when the Promise is kept. The callbacks are executed using a form of multi-dispatch, so multiple alternative callbacks can be provided and the callback chosen is based upon its signature.

The action of the callbacks is based on the method used to register them:

Each of these return a new Future that will be fulfilled after the original Future is fulfilled and the callback completes. The actual semantics of how each these calls work is subtly different, but are loosely based upon the differences in how Perl 6 handles multi-subs, CATCH-blocks, and LAST-blocks.

A Future is also type-aware. I often want to return a Promise from a method, but then I have to explicitly document what that Promise is actually supposed to return. This is no longer a problem with Future:

# This is bleh
method fib($n --> Promise) { ... }

# This is nice
method fib($n --> Future[Int]) { ... }

You can create a Future with an explicitly parameterized type or you can use the .constrain() method to take an existing Future and apply that type expectation. The latter should be done at the end of a callback chain because it's only the final fulfillment that ought to be constrained to the final result type. (Though, you may, of course, constrain the intermediate steps if you like.)

Finally, a Future will recursively await anything that is await-able. All concurrent objects built-in to Perl 6 provide an await function that can be used to wait for a value from another thread to become available whtin the current thread. This means that any time a Future encounters an object that can be awaited, it will await that return before continuing.

GOALS

Why has this module been created? This module's purpose is to create an interface based on Promise that has a few features which can make it nicer to work with as a developer. These goals are based upon my own experience working with promise-type tools in other languages, such as Perl 5, JavaScript, and Scala. It does not try to implement a particular interface from that experience, but tries to remain true to idiomatic Perl 6 in style and implementation instead.

Here are the primary goals I had in mind, contrasted with the behavior of Promise:

FUTURE STATES

A Future has three possible states:

While a particular Future object is unable to transition out of a fulfilled or rejected stop state, you can transform the Future using a callback. Callbacks will construct a new future, which can have a different state than the one the callback starts with.

METHODS

method awaitable

method awaitable(Future:U: $p --> Future:D)

This constructs and returns a new Future based around the given Promise or other awaitable object. The Future will be fulfilled or rejected whenever the underlying Promise or other await-able is fulfilled or rejected.

The following is mostly a reiteration of the documentation for await, but here's a quick rundown of how each behaves when wrapped in a Future.

Promise

The Future will remain pending as long as the Promise is Planned. The Future will become fulfilled when the Promise is Kept or rejected when the Promise is broken.

Channel

The Future remains pending until the next value is sent to the underlying Channel. At that time, the Future becomes fulfilled with that value. If the Channel is closed before a value is sent, the Future is rejected.

Supply

The Future is held pending until the Supply emits all values and is done. The Future will become fulfilled with the final value emitted at that point. If, isntead, the Supply quits, the Future becomes rejected.

method start

method start(Future:U: &block --> Future:D)

You can think of this as a shortcut for:

Future.awaitable(start { ... });

The given &block will be scheduled to run on the next available thread. Then, this method constructs and returns a Future whose outcome will be based upon the outcome of that block. If the block exits normally, the result of the block will become the fulfilled Future value. If, instead, the block throws an exception, the Future is rejected with that exception.

method immediate

method immediate(Future:U: $value --> Future:D)

This method constructs and returns a Future that is already fulfilled with the given value.

method exceptional

method exceptional(Future:U: Exception $x --> Future:D)

This method constructs and returns a Future that is already rejected with the given Exception.

method is-pending

method is-pending(Future:D: --> Bool)

Returns True while the Future is pending and not yet either fulfilled or rejected. Therefore, the following invariant will always be true:

.is-pending ?? !.is-fulfilled && !.is-rejected !! .is-fulfilled || .is-rejected

Once this becomes False, it will always be False.

method is-fulfilled

method is-fulfilled(Future:D: --> Bool)

Returns True if the Future has been fulfilled with a value. Once this becomes True it will always be True.

method is-rejected

method is-rejected(Future:D: --> Bool)

Returns True if the Future has been rejected with an Exception. Once this becomes True it will always be True.

method then

method then(Future:D: *@callbacks --> Future:D)

This method constructs and returns a new Future. When the original invocant is fulfilled, a callback will be selected based on the outcome of the original Future's fulfillment. For example:

my $v = await Future.start({ 41 }).then(
    -> Str $s { $s.Int },
    -> Int $i { $i + 1 },
);
say $v; #> 42

The callback with a signature matching the original value will be chosen. Exactly one callback must apply to the value or an exception will be thrown. If multiple callbacks may apply, you should use the is default trait to distinguish which is preferred. The goal here is to act as much like a multi-sub as possible. (Though, the exceptions thrown are X::Future::NoMatch and X::Future::Ambiguous rather than the Perl counterparts for multis.)

The outcome of the callback is then used to determine the outcome of the newly constructed Future. If the block exits normally, the resulting value will be the fulfilled value of the Future. If the block exits with an exception, that exception will be used for the rejection of that Future.

If the Future is rejected, none of these callbacks are called and the Future becomes rejected with the same exception as the original invocant:

await Future.start({ die 42 }).then(
    -> Str $s { $s.Int }, # ignored
    -> Int $i { $i + 1 }, # ignored
);

CATCH { .payload.say }
#> 42

When going from one Future to the next in the .then() callback chain, the results of each Future earlier in the chain is treated as a Capture. This means that by using Capture objects directly (or, if you prefer, lists or hashes to create Capture-like objects indirectly), you can very nicely chain complex results from one Future to the next:

Future.start({ \(42, :!beeblebrox) }).then(
    -> $answer, :$beeblebrox = True { ... }
});

method catch

method catch(Future:D: *@callbacks --> Future:D)

This method constructs and returns a new Future. When the original invocant is rejected, a callback will be selected based on teh outcome of the original Future's rejection. For example:

my $v = await Future({ die 42 }).then(
    -> X::AdHoc $x { .payload }),
    -> X::IO $x { $x.rethrow }),
);
say $v; #> 42

The callback with a signature matching the original Future's rejected Exception value will be chosen. The first matching callback is accepted and it is acceptable for no callback to match (in which case, the returned Future will be rejected with the same exception as the original). The goal is to act as much as possible like a CATCH-block.

The outcome of the callback is used to determine the outcome of the newly constructure Future. If the block exits normally, the resulting value will be the fulfilled value of the Future. If the block exits with an exception, that exception will be used for the rejection of that Future.

If the original Future is fulfilled, the callbacks will be ignored and the new Future in the chain will be fulfilled with the same value as the previous.

method constrain

method constrain(Future:D: Mu \type --> Future:D)

Given a type name, this constructs a new Future constrained to that type. It is most useful in situations where you want to add a type constraint on the end-product but the intermediate steps in the callback chain can be something different:

# This example is trivial, but makes the point
sub calculate(Int $i --> Future[Str]) {
    Future.start({
        (1..*).grep(*.is-prime)[$i]
    }).then({ .Str }).constrain(Str);
}

my Future[Str] $p = calculate(10_000_000_000);
say await $p;

It constrains the type. It is up to the Future to make sure the correct type is actually fulfilled. If the fulfilled type does not match the constraint, the Future is rejected with an X::Future::Mismatch exception.

method result

method result(Future:D: --> Mu)

This will return the fulfilled result of the Future or throw the Exception the future was rejected with. This will block until a value becomes available.

sub await

sub await(Future:D $future --> Mu)

This will return the fulfilled result of the Future or throw the Exception the future was rejected with. This will block until a value becomes available.

DIAGNOSTICS

Here are the exceptions that are specifically added by this class. Please note that there may be other exceptions thrown by a Future that are just standard Perl exceptions. See the documentation of exceptions for Perl and Rakudo, especially regarding exceptions that may be thrown by await.

X::Future

All of the special exceptions that may be thrown by the Future itself will implement this base exception.

X::Future::NoMatch

This is roughly equivalent to X::Multi::NoMatch and occurs when none of the callbacks listed in a callback chain are unable to handle the incoming value.

X::Future::Ambiguous

This is rougly equivalent to X::Multi::Ambiguous and occurs when more than one callback listed could resolve the callback value. If this could be possible, it is recommended that you add the is default trait to one of the callbacks.

X::Future::Mismatch

This is thrown when the Future is fulfilled with a value that does not match the parameterized type constraint of the Future.

method expected

method expected(X::Future::Mismatch:D: --> Mu)

Returns the type constraint of the Future.

method got

method got(X::Future::Mismatch:D: --> Mu)

Returns the value that failed the type constraint.