Futures
Future<R> is the result-handle for a pending async operation — the consumer side. FutureSlot<R> is the producer side: the runtime resolves a slot when the reply arrives, which in turn resolves the paired Future. Together they form the plumbing that ActorRef::ask() is built on.
The design
Future and FutureSlot
Future wraps a FutureSlot and exposes the read-only contract: await(), isResolved(), cancel(), map(), flatMap(), and onCancel().
FutureSlot is a runtime-specific interface. Each runtime provides its own implementation:
FiberFutureSlot— suspends the current PHP fiber viaFiber::suspend()untilresolve()is called.SwooleFutureSlot— blocks a Swoole coroutine on aChanneluntil the reply arrives.ImmediateFutureSlot— already-resolved slot forFuture::resolved()andFuture::failed().LazyFutureSlot— wraps aClosurefor deferred execution; used internally byFuture::all(),map(), andflatMap().
You do not construct FutureSlot directly. ActorRef::ask() creates one via the runtime and hands the Future back to you.
How ask() uses them
When you call $ref->ask(fn($replyTo) => new GetCount($replyTo), Duration::seconds(5)):
- The runtime creates a
FutureSlotand a temporaryFutureRefwhosetell()calls$slot->resolve(). - The request message is sent to the target actor with
$replyToset to theFutureRef. - A timeout fiber is scheduled; if it fires first,
$slot->fail(new FutureTimeoutException(...)). await()suspends the caller's fiber until the slot is resolved or the timeout fires.
Future::all() for parallel asks
Future::all() collects multiple futures and resolves when all complete, or fails on the first failure.
<?php
declare(strict_types=1);
use Monadial\Nexus\Runtime\Async\Future;
use Monadial\Nexus\Runtime\Async\FutureResult;
use Monadial\Nexus\Runtime\Duration;
$timeout = Duration::seconds(3);
$futures = [
'inventory' => $inventoryRef->ask(fn($r) => new GetStock($r), $timeout),
'pricing' => $pricingRef->ask(fn($r) => new GetPrice($r), $timeout),
];
/** @var FutureResult<object> $results */
$results = Future::all($futures)->await();
$stock = $results->values['inventory'];
$price = $results->values['pricing'];
Transforming results lazily
map() and flatMap() transform the result without blocking:
<?php
declare(strict_types=1);
use Monadial\Nexus\Runtime\Async\Future;
use Monadial\Nexus\Runtime\Duration;
/** @var Future<OrderConfirmation> $future */
$future = $orderRef
->ask(fn($r) => new PlaceOrder($items, $r), Duration::seconds(10))
->map(fn(OrderConfirmation $c) => new OrderSummary($c->orderId));
// Fiber suspends here, not at map()
$summary = $future->await();
Tradeoffs
| You gain | You give up |
|---|---|
| Typed async result with fiber suspension | Requires await() call in a fiber/coroutine context |
Composable with map(), flatMap(), all() | Future::all() fails-fast on the first error; partial results are discarded |
Cancellable with cancel() + onCancel() callbacks | Cancellation is cooperative — the target actor is not notified |
When to reach for it directly vs using ask()
Use ActorRef::ask() for the common case. It constructs the FutureSlot, wires the timeout, and returns the Future in one call.
Work with Future directly when you need:
Future::all()to fan out to multiple actors and collect all replies.Future::resolved()orFuture::failed()to return an already-complete value in a test or stub.map()/flatMap()to chain dependent operations before callingawait().
Implement FutureSlot only when writing a new Runtime — application code never implements this interface.
See also
- Ask pattern — the user-facing API built on top of
Future. - Guides: ask vs tell — when to use ask and when tell is better.
- Runtimes — how
FiberRuntimeandSwooleRuntimeimplementFutureSlot.