Skip to content

Deferred

Deferred<A, E>は、特別なサブタイプのEffectで、変数として機能しますが、一度だけ設定できるというユニークな特性があります。これは非同期操作を管理するための強力な同期ツールです。

Deferredは本質的に即座に利用できないかもしれない値を表す同期プリミティブです。Deferredを作成すると、初めは空の値で始まります。その後、成功値(A)または失敗値(E)のいずれかで、正確に一度だけ完了させることができます。一度完了すると、Deferredは再度変更されたり空にされたりすることはありません。

一般的な使用例

Deferredは、プログラム内で特定の何かが起こるのを待たなければならないときに非常に便利です。コードの一部が準備ができたときに、別の部分に信号を送ることが必要なシナリオに最適です。以下にいくつかの一般的な使用例を示します。

  • ファイバーの調整: 複数の同時タスク(ファイバー)があり、それらのアクションを調整する必要があるとき、Deferredはあるファイバーがタスクを完了したときに別のファイバーに信号を送るのに役立ちます。

  • 同期: あるコードの部分が別の部分の作業が終了するまで進まないことを確認したいとき、Deferredは必要な同期を提供します。

  • 作業の受け渡し: Deferredを使用して、一つのファイバーから別のファイバーに作業を受け渡すことができます。例えば、一つのファイバーがデータを準備し、もう一つのファイバーがそれを処理を続けることができます。

  • 実行の一時停止: 条件が満たされるまでファイバーを一時停止したい場合、Deferredを使用して、その条件が満たされるまでブロックすることができます。

ファイバーがDeferredに対してawaitを呼び出すと、それは基本的にそのDeferredが値またはエラーで完了するまでブロックされます。重要なのは、Effectでは、ブロックされたファイバーが実際にメインスレッドをブロックするのではなく、意味的にのみブロックされることです。一つのファイバーがブロックされている間、基盤となるスレッドは他のファイバーを実行でき、効率的な同時実行性を確保します。

EffectのDeferredは、JavaScriptのPromiseに概念的に似ています。主な違いは、Deferredにはタイプパラメータ(EA)が二つあり、一つだけではないことです。これにより、Deferredは成功した結果(A)とエラー(E)の両方を表すことができます。

操作

作成

DeferredDeferred.make<A, E>()を使用して作成できます。これは、Deferredの作成を説明するEffect<Deferred<A, E>>を返します。DeferredEffect内でのみ作成できることに注意してください。なぜなら、Deferredを作成することは効果的なメモリアロケーションを伴い、安全に管理する必要があるからです。

待機

Deferredから値を取得するには、Deferred.awaitを使用します。この操作は、呼び出しファイバーをDeferredが値またはエラーで完了するまで suspend します。

import { Effect, Deferred } from "effect"
const effectDeferred = Deferred.make<string, Error>()
const effectGet = effectDeferred.pipe(
Effect.andThen((deferred) => Deferred.await(deferred))
)

完了

Deferred<A, E>を様々な方法で完了させることができます。

  • Deferred.succeed: 値を型Aで成功裏にDeferredを完了させます。
  • Deferred.done: Exit<A, E>型でDeferredを完了させます。
  • Deferred.complete: 効果Effect<A, E>の結果でDeferredを完了させます。
  • Deferred.completeWith: 効果Effect<A, E>Deferredを完了させます。この効果は、待機している各ファイバーによって実行されるため、注意が必要です。
  • Deferred.fail: エラー型EDeferredを失敗させます。
  • Deferred.die: ユーザー定義のエラーでDeferredを異常終了させます。
  • Deferred.failCause: Cause<E>Deferredを失敗させるか、異常終了させます。
  • Deferred.interrupt: Deferredを中断します。これにより、待機中のファイバーを強制的に停止または中断できます。

以下の例は、これらの完了メソッドの使用法を示しています。

import { Effect, Deferred, Exit, Cause } from "effect"
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
// 対応する方法で Deferred を完了します
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)
// Deferred の値を取得するために待機する
const value = yield* Deferred.await(deferred)
return value
})
Effect.runPromise(program).then(console.log, console.error) // 出力: 1

Deferredを完了させると、Effect<boolean>の結果が得られます。この効果は、Deferredの値が設定された場合はtrueを返し、完了前にすでに設定されていた場合はfalseを返します。これにより、Deferredの状態を確認することができます。

以下は、Deferredの状態の変化を示す例です。

import { Effect, Deferred } from "effect"
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
const b1 = yield* Deferred.fail(deferred, "oh no!")
const b2 = yield* Deferred.succeed(deferred, 1)
return [b1, b2]
})
Effect.runPromise(program).then(console.log) // 出力: [ true, false ]

ポーリング

時には、ファイバーを一時停止させずにDeferredが完了したかどうかを確認したいことがあります。そのためには、Deferred.pollメソッドを使用します。これがどのように機能するかを説明します。

  • Deferred.pollOption<Effect<A, E>>を返します。
    • Deferredがまだ完了していない場合は、Noneを返します。
    • Deferredが完了している場合は、結果またはエラーを含むSomeを返します。

さらに、Deferred.isDoneメソッドを使用することができ、これはEffect<boolean>を返します。この効果は、Deferredがすでに完了している場合はtrueに評価され、完了状況を迅速に確認できます。

以下は実用的な例です。

import { Effect, Deferred } from "effect"
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<number, string>()
// Deferredをポーリングする
const done1 = yield* Deferred.poll(deferred)
// Deferredがすでに完了しているかどうかを確認する
const done2 = yield* Deferred.isDone(deferred)
return [done1, done2]
})
Effect.runPromise(program).then(console.log) // 出力: [ none(), false ]

この例では、最初にDeferredを作成し、その後Deferred.pollを使用して完了状況を確認します。まだ完了していないため、done1none()です。また、Deferred.isDoneを使用して、Deferredが完了していないことを確認し、done2falseであることを示します。

例: 2つのファイバーを調整するためにDeferredを使用する

以下は、Deferredを使用して2つのファイバー間で値を受け渡すシナリオです。

import { Effect, Deferred, Fiber } from "effect"
const program = Effect.gen(function* () {
const deferred = yield* Deferred.make<string, string>()
// ファイバーA: 1秒待機してからDeferred値を設定
const sendHelloWorld = Effect.gen(function* () {
yield* Effect.sleep("1 second")
return yield* Deferred.succeed(deferred, "hello world")
})
// ファイバーB: Deferredを待機して値を表示
const getAndPrint = Effect.gen(function* () {
const s = yield* Deferred.await(deferred)
console.log(s)
return s
})
// 両方のファイバーを並行して実行
const fiberA = yield* Effect.fork(sendHelloWorld)
const fiberB = yield* Effect.fork(getAndPrint)
// 両方のファイバーが完了するのを待つ
return yield* Fiber.join(Fiber.zip(fiberA, fiberB))
})
Effect.runPromise(program).then(console.log, console.error)
/*
出力:
hello world
[ true, "hello world" ]
*/

この例では、fiberAfiberBという2つのファイバーがDeferredを使って通信します。

  • fiberAは1秒待ってからDeferredの値を”hello world”に設定します。
  • fiberBDeferredが完了するのを待って、その値をコンソールに表示します。

両方のファイバーを並行して実行し、Deferredを同期ポイントとして使用することで、fiberBfiberAがタスクを完了するまで進行しないことが保証されます。この調整メカニズムを使用することで、プログラムの異なる部分間で値を手渡すことや作業を調整することが効果的に行えます。