NAME
Acme::Parataxis::Future - Placeholder for an Eventual Computation Result
SYNOPSIS
use v5.40;
use Acme::Parataxis qw[async fiber];
use Acme::Parataxis::Future;
# A promise for one value, resolved exactly once.
my $reply = Acme::Parataxis::Future->new;
async {
fiber { $reply->set_result( fetch_user( 42 ) ) }; # some other fiber produces it
my $user = $reply->await; # this one suspends until it lands
say $user->{name};
};
The two halves are independent, and neither needs a fiber. A callback registered before the value exists fires when it does, right there in the setter:
my $reply2 = Acme::Parataxis::Future->new;
$reply2->on_ready( sub { my $f = shift; say 'landed: ' . $f->result->{name} } );
$reply2->set_result( $user ); # the callback runs here, before set_result returns
A fresh future takes the failure path, and error tells the two apart without dying:
my $reply3 = Acme::Parataxis::Future->new;
$reply3->set_error( $err );
if ( defined $reply3->error ) { warn 'failed: ' . $reply3->error }
ref $reply3->error; # the error as stored - the same object you set, not a copy
DESCRIPTION
A future is a one-shot slot for a value that does not exist yet. It has two halves that never need to meet: a producer calls set_result or set_error exactly once, and any number of consumers call await to suspend until it happens, or register an on_ready callback to be called when it does. Nothing is required to be a fiber to set_result, so a value arriving on a callback, from a timer, or from plain mainline code resolves a future just as well as a fiber does.
A future transitions from not-ready to ready exactly once. set_result and set_error both refuse a second transition, and clear_result is the only way back - which makes a future a genuine one-shot rather than a mutable box.
Parataxis uses this class itself: Acme::Parataxis::Actor's ask returns one, spawn_blocking returns one, Acme::Parataxis::Monitor hands one back, and wait_all and wait_any build one out of many.
METHODS
new( )
my $future = Acme::Parataxis::Future->new;
A new, not-ready future. The constructor takes no parameters at all - there are no fields to initialise, and it is not a configuration object. Passing anything is a mistake and is reported as one; see "The constructor is strict".
is_ready( )
if ( $future->is_ready ) { ... }
Whether the future has been resolved, by either setter. A future resolved with undef is ready, so this is the only way to tell a resolved-undef from an unresolved future.
error( )
my $err = $future->error;
The stored error, raw: undef unless set_error was called, and afterwards exactly the value that was passed in. Read it to tell a failure from a result without dying, which is what on_ready is for - result croaks instead.
Note that undef is ambiguous. Both set_result(undef) and set_error(undef) leave error undef, and both leave result returning undef; see "set_error(undef) is indistinguishable from a successful undef".
result( )
my $val = $future->result; # croaks unless the future is resolved successfully
The result, immediately, without suspending. Croaks Future not ready if the future has not been resolved - use is_ready, or await to wait for it - and croaks the stored error if it was resolved with set_error.
The error croak stringifies. If you stored an object, $@ is a string, not that object; read error when you need the object itself. See "A failed result croaks the error as a string, not as the object you stored".
set_result( $value )
$future->set_result($value); # fires every on_ready callback, then wakes every awaiter
Resolve the future with a successful value. Any registered on_ready callbacks run here, synchronously and in registration order, before any parked fiber is scheduled. Every parked await is then woken, and returns this value.
undef is a perfectly good value. The second and later resolutions die with Future already ready, whether the first was a result or an error. Returns undef; the return value is not meaningful.
set_error( $error )
$future->set_error($err); # same wakeup path, but await and result die with $err
Resolve the future with a failure. The wakeup path is identical to set_result - callbacks fire, then waiters are woken - but await and result die with the stored error instead of returning it.
undef is accepted, and a future resolved with set_error(undef) is ready with no error and an undefined result: "set_error(undef) is indistinguishable from a successful undef". Dies with Future already ready on a second resolution. Returns undef; the return value is not meaningful.
on_ready( $callback )
$future->on_ready( sub { my $f = shift; say $f->result } );
Register a callback, or pass one to a future that is already ready - in which case it runs immediately, before on_ready returns. Callbacks receive the future, so one registered callback can read the result without closing over it.
Callbacks fire in registration order, synchronously, from inside the setter. They fire before any parked fiber is resumed, because waking a fiber only makes it runnable; the callbacks have all returned by the time the first waiter is scheduled again.
await( )
my $val = $future->await; # suspends the calling fiber
Suspend the current fiber until the future is ready, then return the result. If the future is already ready this returns without suspending, which is also the only case where it works outside a running fiber: from the mainline a pending future croaks await() must be called from inside a scheduled fiber.
If the future was resolved with set_error, await dies with the stored error, stringified in the same way result does it. Multiple fibers may await the same future; each parks and each is woken. If a token is cancelled while a fiber is parked here, the wait re-enters and throws Acme::Parataxis::Error::Cancelled, and the fiber deregisters itself, so a later resolution will not try to wake it.
remove_waiter( $fid )
my $removed = $future->remove_waiter($fid);
Unregister a fiber that is parked in await, so a later set_result or set_error will not wake it. Returns the number of entries removed, 0 or 1. A fiber removed this way is not resumed by anything, so the caller owns it and has to reclaim it.
You rarely need this by hand: an interrupted await unregisters itself, which is what lets a cancelled fiber be collected while its future stays usable.
clear_result( )
$future->clear_result; # back to not-ready, with no callbacks and no waiters
Reset the future to not-ready, discarding the stored result, the stored error, every registered on_ready callback, and every parked waiter. The future can then be resolved and awaited again, which is the supported way to reuse one.
Clearing a future that has waiters parked in it is a deadlock, not a reset - see "clear_result drops parked waiters, and they are never resumed".
Recipes
Resolve from a worker, consume from many
my $future = Acme::Parataxis::Future->new;
async {
my $worker = fiber { $future->set_result( expensive() ) };
my @consumers = map { fiber { $future->await } } 1 .. 3; # three fibers, one slot
say join ', ', map { $_->await } @consumers;
$worker->await;
};
Every waiter is woken from the same resolution, so a fan-out costs one producer and three cheap parks. The fiber returned by fiber also answers await, so $_->await is the natural way to collect them.
Give the producer a bound, so it cannot outlive its consumer
my $future = Acme::Parataxis::Future->new;
async {
my $worker = fiber {
my $ok = eval { with_timeout( 5_000, sub { $future->set_result( work() ) } ); 1 };
warn "producer gave up: $@" unless $ok; # nobody ever resolved the future
};
# and so does the consumer
if ( my $err = do { eval { with_timeout( 250, sub { $future->await } ) }; $@ } ) {
warn "gave up: $err";
}
else {
say $future->result->{name};
}
$worker->await; # returns once the producer has unwound at its own bound
};
A fiber cannot be stopped from the outside. There is no stop or cancel method on one: stop exists in Acme::Parataxis but it is a function that ends the whole run, and it shares a package name with the fiber class, so $fiber->can('stop') answers true and the call still fails. A producer that might outlive its consumer therefore needs a bound of its own, and a future nobody will ever resolve is a perfectly ordinary way to hang a fiber.
Acme::Parataxis's with_cancel is the on-demand alternative, and it has a sharp edge worth knowing before you reach for it. The scope token is the block's own argument, and it is also the return value - but the return value is only bound once the block has finished, which is exactly too late to cancel it. Publish the argument from inside the block instead:
use Acme::Parataxis qw[async fiber yield with_cancel with_timeout]; # both are in @EXPORT_OK
my ( $scope, $outcome ) = ( undef, undef );
async {
my $worker = fiber {
with_cancel( sub ($t) { $scope = $t; $future->set_result( work() ) } );
};
yield; # let the block publish its token
$scope->cancel; # interrupts whatever the producer is parked on
$outcome = eval { $worker->await; 1 } ? 'returned' : ( split /\n/, $@ )[0];
};
# $outcome is now "The operation was cancelled at await_sleep (...)"
The block's argument is available from its first statement, so this works. Capturing the return value instead - my $scope = with_cancel(sub {...}) - leaves $scope empty for the whole life of the block, and hands you an inert token once it is over.
Note the shape of the consumer's own await: a timed-out or cancelled wait unregisters itself from the future on the way out, so a dead wait never leaves the future holding a reference to it. That is the wait's cleanup hook, and it is why you do not need remove_waiter on the abandoned path.
Report a failure without tripping over it
$future->on_ready( sub {
my $f = shift;
if ( defined $f->error ) { warn 'job failed: ' . $f->error; return }
warn "job returned: " . ( $f->result // '(undef)' );
} );
on_ready is the non-throwing way to observe a future. It fires for a failure as readily as for a result, and error separates the two.
Behavior Notes
clear_result drops parked waiters, and they are never resumed
clear_result empties the waiter list as well as the callback list. A fiber parked in await is not woken, is not told anything, and is not re-registered when the future is resolved again - it is simply gone from the future's bookkeeping. The run then has a live fiber that nothing can ever resume:
my $future = Acme::Parataxis::Future->new;
my $w = fiber { $future->await };
yield; # $w is parked
$future->clear_result; # $w is dropped from the waiter list
$future->set_result('value'); # wakes an empty list
$w->await; # FATAL: deadlock detected
Recycle a future with clear_result only when nothing is parked on it. The same applies to on_ready: a callback registered but not yet fired is discarded silently, so a callback you are still counting on can simply never run.
A throwing on_ready callback aborts the rest and escapes the setter
Callbacks run in a plain loop inside the setter, so the first one to die takes the others with it and propagates out:
$future->on_ready( sub { die "callback exploded\n" } );
$future->on_ready( sub { say 'never reached' } );
$future->set_result(1); # dies with "callback exploded", before the second callback
The future is already marked ready by then, so the second registration cannot be retried and the second callback is simply lost. Keep on_ready callbacks total: catch inside them, or wrap the body in an eval that logs.
A failed result croaks the error as a string, not as the object you stored
result and await surface a failure with croak $error. croak stringifies its argument and appends the caller's location, so the object does not survive the trip:
my $real = Acme::Parataxis::Error::Timeout->new;
$future->set_error($real);
eval { $future->await };
ref $@; # '' - a plain string like "The operation timed out at ... at t/job.pl line 31.\n"
ref $future->error; # 'Acme::Parataxis::Error::Timeout' - the object is still there, if you want it
Do not compare two errors with ==. They overload stringification, not numerics, so it compares the two messages and warns isn't numeric in numeric eq on both sides; see "The overload is a string, not a number" in Acme::Parataxis::Error.
Read error rather than $@ whenever the error is an object you need to inspect. This matters for Acme::Parataxis::Error subclasses specifically, because ->kind and ->wait_reason live on the object and are gone from the string.
set_error(undef) is indistinguishable from a successful undef
set_error does not reject undef, and it does not reject it specially either. A future resolved that way is ready, has no error, and returns undef from result - exactly what set_result(undef) gives you. Through the public API the two are the same object, so a future cannot be used to signal "failed, with nothing to report". If that distinction matters, resolve with a value that is not undef.
The constructor is strict
new has no parameters, and the generated constructor says so:
Acme::Parataxis::Future->new( name => 'job' );
# Warning: Odd number of arguments passed to "Acme::Parataxis::Future" constructor
# Unrecognized parameters for "Acme::Parataxis::Future" constructor: name
There is no field to name and no option to set, so this is only worth knowing to the extent that it is a loud failure rather than a silently ignored argument.
A fiber speaks a similar protocol, and it is not this one
Acme::Parataxis fiber objects carry their own result, set_result, set_error, on_ready, await, is_ready and error, and they are a separate class - a fiber is not a Acme::Parataxis::Future, and wait_all and wait_any reject one. The two are not interchangeable:
A fiber's
set_resulton an already-ready fiber is silently ignored. AFuturedies withFuture already ready.A fiber's
resulton a failed fiber returnsundefwithout raising anything. AFuturecroaks the stored error.A fiber holds one waiter. A
Futureholds any number, which is what makes fan-out andwait_allpossible; two fibers awaiting one fiber leave both of them parked.A fiber has no
clear_resultand noremove_waiter.
Prefer a Future for a value you are coordinating; reach for the fiber itself when you want a running fiber.
SEE ALSO
Acme::Parataxis, Acme::Parataxis::Error, Acme::Parataxis::Actor, Acme::Parataxis::Monitor
AUTHOR
Sanko Robinson https://github.com/sanko
LICENSE
Copyright (C) Sanko Robinson.
This library is free software; you can redistribute it and/or modify it under the terms found in the Artistic License 2.