Deferred
Deferred<A> is a purely functional, write-once synchronization primitive. It starts empty and can be completed with a value exactly once. Any fiber that calls value() before the value is available will suspend until another fiber calls complete, at which point all waiting fibers are unblocked simultaneously.
INFO
Deferred is to IO what Completer is to Future — a way to hand a result from one fiber to another. Unlike Completer, Deferred is referentially transparent: allocation and completion are both IO effects.
How it differs from Queue
Queue and Deferred both let fibers communicate, but they serve different needs:
Queue<A> | Deferred<A> | |
|---|---|---|
| Capacity | Holds many values | Holds exactly one value, forever |
| Consumers | Each value is taken by one consumer | All waiting consumers unblock at once |
| Reuse | Can send many messages | Write-once — completed value is permanent |
| Use for | Streams of work items | One-shot signals and startup coordination |
Core operations
| Method | Returns | Description |
|---|---|---|
Deferred.of<A>() | IO<Deferred<A>> | Allocate a new, empty Deferred |
complete(a) | IO<bool> | Set the value; true if this was the first call, false if already set |
value() | IO<A> | Get the value, suspending if not yet available |
tryValue() | IO<Option<A>> | Get the value immediately as Option — None if not yet completed |
Basic usage
IO<Unit> deferredBasic() => Deferred.of<int>().flatMap(
(d) =>
IO
.both(
// Consumer: suspends on value() until the Deferred is completed
d.value().flatMap((n) => IO.print('got: $n')),
// Producer: completes the Deferred after a short delay
IO
.sleep(100.milliseconds)
.productR(d.complete(42))
.flatMap((won) => IO.print('completed: $won')), // completed: true
)
.voided(),
);The consumer fiber suspends on value() immediately. When the producer calls complete(42) after the sleep, the consumer is woken up and proceeds.
Write-once guarantee
complete is idempotent after the first call — subsequent calls return false and have no effect. The value set by the first caller is preserved permanently:
IO<Unit> completeOnce() => Deferred.of<String>().flatMap(
(d) => IO
.both(
d.complete('first').flatMap((won) => IO.print('first: $won')), // first: true
d.complete('second').flatMap((won) => IO.print('second: $won')), // second: false
)
.flatMap((_) => d.value().flatMap((v) => IO.print('value: $v'))),
); // value: firstThis makes Deferred safe to use in races: whichever fiber wins the complete call sets the canonical result, and all other attempts are silently ignored.
Real-world example: service startup gate
A common pattern in concurrent programs is to start several fibers that depend on a shared resource that takes time to initialize. Rather than polling or sleeping, a Deferred acts as a gate: dependents call value() and suspend; the initializer calls complete once, instantly releasing every waiting fiber.
The example below models a service that publishes its endpoint once it has finished starting up. Three client fibers wait on the same Deferred — they all unblock the moment the service is ready, without any polling:
/// A service publishes its endpoint via [Deferred] once startup is done.
/// Any number of client fibers can call [ready.value()] and will all unblock
/// at the same instant, the moment [complete] is called.
IO<Unit> serviceReadyGate() => Deferred.of<String>().flatMap((ready) {
IO<Unit> client(int id) =>
ready.value().flatMap((endpoint) => IO.print('client $id: connected to $endpoint'));
final service = IO
.sleep(200.milliseconds)
.productR(ready.complete('https://api.example.com'))
.productR(IO.print('service: startup complete, clients unblocked'));
return ilist([client(1), client(2), client(3), service]).parSequence_();
});Note that it doesn't matter how many clients are waiting — the single complete call broadcasts the result to all of them in one step. This is the key advantage over Queue, which would require the service to offer once per client.