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:
-
The
.start()
method takes a block which will run on a new thread. The return value from the block becomes the future value. -
The
.awaitable()
method takes any object that can be used withawait
. The Future will get the value of that object wheneverawait
returns it. -
The
.immediate()
takes a value which immediately fulfills the Future. -
The
.exceptional()
takes an Exception, which creates a rejected Future.
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:
-
.then()
These callbacks are executed on fulfillment. -
.catch()
These callbacks are executed on rejection.
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:
-
- A Future should have the ability to use chains of callback to handle exceptional conditions. A Promise does not call any callbacks if the Promise is broken. To handle an exception, you must do something like nested
start
blocks with the outer start blockawait
ing an inner block and catching the exception that may be rethrown.
- A Future should have the ability to use chains of callback to handle exceptional conditions. A Promise does not call any callbacks if the Promise is broken. To handle an exception, you must do something like nested
-
- A Future should be parameterized by type. A Promise may be kept with any kind of value. One advantage of a type-system is that it is self-documenting. By examining the types accepted as parameters or returned, its often possible to infer what a subroutine does. A Promise provides some useful information about the immediate type, but no indication as to what it could become in the future.
FUTURE STATES
A Future has three possible states:
-
- Pending. A pending Future is one that may either become fulfilled or become rejected. It might also remain pending indefinitely. This corresponds to the Planned state of Promise.
-
- Fulfilled. A fulfilled Future is complete and has a value. This is a stop state and the Future is not able to transition out of this status. This corresponds to the Kept state of Promise.
-
- Rejected. A rejected Future is failed with an exception. This is a stop state and the Future is not able to transition out of this status. This corresponds to the Broken state of Promise.
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.