Deferred
A Deferred<E, A>
is a special subtype of Effect
that acts as a variable, but with some unique characteristics. It can only be set once, making it a powerful synchronization tool for managing asynchronous operations.
A Deferred
is essentially a synchronization primitive that represents a value that may not be available immediately. When you create a Deferred
, it starts with an empty value. Later on, you can complete it exactly once with either a success value (A
) or a failure value (E
). Once completed, a Deferred
can never be modified or emptied again.
Common Use Cases
Deferred
becomes incredibly useful when you need to wait for something specific to happen in your program. It's ideal for scenarios where you want one part of your code to signal another part when it's ready. Here are a few common use cases:
-
Coordinating Fibers: When you have multiple concurrent tasks (fibers) and need to coordinate their actions,
Deferred
can help one fiber signal to another when it has completed its task. -
Synchronization: Anytime you want to ensure that one piece of code doesn't proceed until another piece of code has finished its work,
Deferred
can provide the synchronization you need. -
Handing Over Work: You can use
Deferred
to hand over work from one fiber to another. For example, one fiber can prepare some data, and then a second fiber can continue processing it. -
Suspending Execution: When you want a fiber to pause its execution until some condition is met, a
Deferred
can be used to block it until the condition is satisfied.
When a fiber calls await
on a Deferred
, it essentially blocks until that Deferred
is completed with either a value or an error. Importantly, in Effect, blocking fibers don't actually block the main thread; they block only semantically. While one fiber is blocked, the underlying thread can execute other fibers, ensuring efficient concurrency.
A Deferred
in Effect is conceptually similar to JavaScript's Promise
. The key difference is that Deferred
has two type parameters (E
and A
) instead of just one. This allows Deferred
to represent both successful results (A
) and errors (E
).
Operations
Creating
You can create a Deferred
using Deferred.make<E, A>()
. This returns an Effect<never, never, Deferred<E, A>>
, which describes the creation of a Deferred
. Note that Deferred
s can only be created within an Effect
because creating them involves effectful memory allocation, which must be managed safely within an Effect
.
Awaiting
To retrieve a value from a Deferred
, you can use Deferred.await
. This operation suspends the calling fiber until the Deferred
is completed with a value or an error.
import { Effect, Deferred } from "effect"
// $ExpectType Effect<never, never, Deferred<Error, string>>
const effectDeferred = Deferred.make<Error, string>()
// $ExpectType Effect<never, Error, string>
const effectGet = effectDeferred.pipe(
Effect.flatMap((deferred) => Deferred.await(deferred))
)
Completing
You can complete a Deferred
[E, A] in different ways:
There are several ways to complete a Deferred
:
Deferred.succeed
: Completes theDeferred
successfully with a value of typeA
.Deferred.done
: Completes theDeferred
with anExit<E, A>
type.Deferred.complete
: Completes theDeferred
with the result of an effectEffect<never, E, A>
.Deferred.completeWith
: Completes theDeferred
with an effectEffect<never, E, A>
. This effect will be executed by each waiting fiber, so use it carefully.Deferred.fail
: Fails theDeferred
with an error of typeE
.Deferred.die
: Defects theDeferred
with a user-defined error.Deferred.failCause
: Fails or defects theDeferred
with aCause<E>
.Deferred.interrupt
: Interrupts theDeferred
. This can be used to forcefully stop or interrupt the waiting fibers.
Here's an example that demonstrates the usage of these completion methods:
import { Effect, Deferred, Exit, Cause } from "effect"
const program = Effect.gen(function* (_) {
const deferred = yield* _(Deferred.make<string, number>())
// Completing the Deferred in various ways
yield* _(Deferred.succeed(deferred, 1).pipe(Effect.fork))
yield* _(Deferred.complete(deferred, Effect.succeed(2)).pipe(Effect.fork))
yield* _(Deferred.completeWith(deferred, Effect.succeed(3)).pipe(Effect.fork))
yield* _(Deferred.done(deferred, Exit.succeed(4)).pipe(Effect.fork))
yield* _(Deferred.fail(deferred, "5").pipe(Effect.fork))
yield* _(
Deferred.failCause(deferred, Cause.die(new Error("6"))).pipe(Effect.fork)
)
yield* _(Deferred.die(deferred, new Error("7")).pipe(Effect.fork))
yield* _(Deferred.interrupt(deferred).pipe(Effect.fork))
// Awaiting the Deferred to get its value
const value = yield* _(Deferred.await(deferred))
return value
})
Effect.runPromise(program).then(console.log, console.error) // Output: 1
When you complete a Deferred
, it results in an Effect<never, never, boolean>
. This effect returns true
if the Deferred
value has been set and false
if it was already set before completion. This can be useful for checking the state of the Deferred
.
Here's an example demonstrating the state change of a Deferred
:
import { Effect, Deferred } from "effect"
const program = Effect.gen(function* (_) {
const deferred = yield* _(Deferred.make<string, number>())
const b1 = yield* _(Deferred.fail(deferred, "oh no!"))
const b2 = yield* _(Deferred.succeed(deferred, 1))
return [b1, b2]
})
Effect.runPromise(program).then(console.log) // Output: [ true, false ]
Polling
Sometimes, you may want to check whether a Deferred
has been completed without causing the fiber to suspend. To achieve this, you can use the Deferred.poll
method. Here's how it works:
Deferred.poll
returns anOption<Effect<never, E, A>>
.- If the
Deferred
is not yet completed, it returnsNone
. - If the
Deferred
is completed, it returnsSome
, which contains the result or error.
- If the
Additionally, you can use the Deferred.isDone
method, which returns an Effect<never, never, boolean>
. This effect evaluates to true
if the Deferred
is already completed, allowing you to quickly check the completion status.
Here's a practical example:
import { Effect, Deferred } from "effect"
const program = Effect.gen(function* (_) {
const deferred = yield* _(Deferred.make<string, number>())
// Polling the Deferred
// $ExpectType Option<Effect<never, string, number>>
const done1 = yield* _(Deferred.poll(deferred))
// Checking if the Deferred is already completed
const done2 = yield* _(Deferred.isDone(deferred))
return [done1, done2]
})
Effect.runPromise(program).then(console.log) // Output: [ none(), false ]
In this example, we first create a Deferred
and then use Deferred.poll
to check its completion status. Since it's not completed yet, done1
is none()
. We also use Deferred.isDone
to confirm that the Deferred
is indeed not completed, so done2
is false
.
Example: Using Deferred
to Coordinate Two Fibers
Here's a scenario where we use a Deferred
to hand over a value between two fibers:
import { Effect, Deferred, Fiber } from "effect"
const program = Effect.gen(function* (_) {
const deferred = yield* _(Deferred.make<string, string>())
// Fiber A: Set the Deferred value after waiting for 1 second
const sendHelloWorld = Effect.gen(function* (_) {
yield* _(Effect.sleep("1 seconds"))
return yield* _(Deferred.succeed(deferred, "hello world"))
})
// Fiber B: Wait for the Deferred and print the value
const getAndPrint = Effect.gen(function* (_) {
const s = yield* _(Deferred.await(deferred))
console.log(s)
return s
})
// Run both fibers concurrently
const fiberA = yield* _(Effect.fork(sendHelloWorld))
const fiberB = yield* _(Effect.fork(getAndPrint))
// Wait for both fibers to complete
return yield* _(Fiber.join(Fiber.zip(fiberA, fiberB)))
})
Effect.runPromise(program).then(console.log, console.error)
/*
Output:
hello world
[ true, "hello world" ]
*/
In this example, we have two fibers, fiberA
and fiberB
, that communicate using a Deferred
:
fiberA
sets theDeferred
value to "hello world" after waiting for 1 second.fiberB
waits for theDeferred
to be completed and then prints the received value to the console.
By running both fibers concurrently and using the Deferred
as a synchronization point, we can ensure that fiberB
only proceeds after fiberA
has completed its task. This coordination mechanism allows you to hand over values or coordinate work between different parts of your program effectively.