Skip to content

Effectの制御フロー演算子入門

JavaScriptは組み込みの制御フローストラクチャを提供していますが、EffectはEffectアプリケーションに役立つ追加の制御フローファンクションを提供します。このセクションでは、実行フローを制御するためのさまざまな方法を紹介します。

if式

Effect値を扱う際には、標準のJavaScriptのif-then-else式を使用できます:

import { Effect, Option } from "effect"
const validateWeightOption = (
weight: number
): Effect.Effect<Option.Option<number>> => {
if (weight >= 0) {
return Effect.succeed(Option.some(weight))
} else {
return Effect.succeed(Option.none())
}
}

ここでは、有効な値がないことを表すためにOptionデータタイプを使用しています。

無効な入力をエラーチャンネルを使用して処理することもできます:

import { Effect } from "effect"
const validateWeightOrFail = (
weight: number
): Effect.Effect<number, string> => {
if (weight >= 0) {
return Effect.succeed(weight)
} else {
return Effect.fail(`negative input: ${weight}`)
}
}

条件演算子

when

if (condition) expressionを使用する代わりに、Effect.when関数を使用できます:

import { Effect, Option } from "effect"
const validateWeightOption = (
weight: number
): Effect.Effect<Option.Option<number>> =>
Effect.succeed(weight).pipe(Effect.when(() => weight >= 0))

ここでも、Optionデータタイプを用いて有効な値の不存在を表示しています。

条件がtrueに評価されると、Effect.when内のエフェクトが実行され、その結果はSomeでラップされます。それ以外の場合はNoneが返されます:

import { Effect, Option } from "effect"
const validateWeightOption = (
weight: number
): Effect.Effect<Option.Option<number>> =>
Effect.succeed(weight).pipe(Effect.when(() => weight >= 0))
// ---cut---
Effect.runPromise(validateWeightOption(100)).then(console.log)
/*
Output:
{
_id: "Option",
_tag: "Some",
value: 100
}
*/
Effect.runPromise(validateWeightOption(-5)).then(console.log)
/*
Output:
{
_id: "Option",
_tag: "None"
}
*/

条件自体がエフェクトを含む場合、Effect.whenEffectを使用できます。

例えば、次の関数では整数の値のランダムオプションを生成します:

import { Effect, Random } from "effect"
const randomIntOption = Random.nextInt.pipe(
Effect.whenEffect(Random.nextBoolean)
)

unless

Effect.unlessおよびEffect.unlessEffect関数は、when*関数に似ていますが、if (!condition) expression構文に相当します。

if

Effect.if関数を使用すると、効果を持つ条件を提供できます。条件がtrueに評価されると、onTrueのエフェクトが実行されます。そうでない場合は、onFalseのエフェクトが実行されます。

この関数を使用して、シンプルな仮想コイン投げ関数を作成してみましょう:

import { Effect, Random, Console } from "effect"
const flipTheCoin = Effect.if(Random.nextBoolean, {
onTrue: () => Console.log("Head"),
onFalse: () => Console.log("Tail")
})
Effect.runPromise(flipTheCoin)

この例では、Random.nextBooleanを使用してランダムなブール値を生成しています。値がtrueの場合、エフェクトonTrueが実行され、「Head」とログが記録されます。そうでなければ、値がfalseの場合、エフェクトonFalseが実行され、「Tail」と記録されます。

ジップ操作

zip

Effect.zip関数を使用すると、2つのエフェクトを1つのエフェクトに組み合わせることができます。この組み合わせたエフェクトは、両方の入力エフェクトの結果をタプルとして返します:

import { Effect } from "effect"
const task1 = Effect.succeed(1).pipe(
Effect.delay("200 millis"),
Effect.tap(Effect.log("task1 done"))
)
const task2 = Effect.succeed("hello").pipe(
Effect.delay("100 millis"),
Effect.tap(Effect.log("task2 done"))
)
const task3 = Effect.zip(task1, task2)
Effect.runPromise(task3).then(console.log)
/*
Output:
timestamp=... level=INFO fiber=#0 message="task1 done"
timestamp=... level=INFO fiber=#0 message="task2 done"
[ 1, 'hello' ]
*/

Effect.zipはエフェクトを逐次処理することに注意してください:まず左側のエフェクトを完了させ、その後右側のエフェクトを完了させます。

エフェクトを同時に実行したい場合は、concurrentオプションを使用できます:

import { Effect } from "effect"
const task1 = Effect.succeed(1).pipe(
Effect.delay("200 millis"),
Effect.tap(Effect.log("task1 done"))
)
const task2 = Effect.succeed("hello").pipe(
Effect.delay("100 millis"),
Effect.tap(Effect.log("task2 done"))
)
// ---cut---
const task3 = Effect.zip(task1, task2, { concurrent: true })
Effect.runPromise(task3).then(console.log)
/*
Output:
timestamp=... level=INFO fiber=#3 message="task2 done"
timestamp=... level=INFO fiber=#2 message="task1 done"
[ 1, 'hello' ]
*/

zipWith

Effect.zipWith関数は、Effect.zipと同様に2つのエフェクトを組み合わせます。ただし、タプルを返すのではなく、組み合わされたエフェクトの結果に関数を適用して単一の値に変換することができます:

import { Effect } from "effect"
const task1 = Effect.succeed(1).pipe(
Effect.delay("200 millis"),
Effect.tap(Effect.log("task1 done"))
)
const task2 = Effect.succeed("hello").pipe(
Effect.delay("100 millis"),
Effect.tap(Effect.log("task2 done"))
)
const task3 = Effect.zipWith(
task1,
task2,
(number, string) => number + string.length
)
Effect.runPromise(task3).then(console.log)
/*
Output:
timestamp=... level=INFO fiber=#3 message="task1 done"
timestamp=... level=INFO fiber=#2 message="task2 done"
6
*/

ループ演算子

loop

Effect.loop関数を使用すると、step関数に基づいて状態を繰り返し変更できます。while関数によって指定された条件がtrueと評価されるまで実行します:

Effect.loop(initial, options: { while, step, body })

すべての中間状態は配列に収集され、最終結果として返されます。

Effect.loopはJavaScriptのwhileループに相当すると考えることができます:

let state = initial
const result = []
while (options.while(state)) {
result.push(options.body(state))
state = options.step(state)
}
return result

import { Effect } from "effect"
const result = Effect.loop(
1, // 初期状態
{
while: (state) => state <= 5, // ループを続ける条件
step: (state) => state + 1, // 状態の更新関数
body: (state) => Effect.succeed(state) // 各反復で実行されるエフェクト
}
)
Effect.runPromise(result).then(console.log) // Output: [1, 2, 3, 4, 5]

この例では、ループは初期状態1から始まります。ループは条件n <= 5trueである限り続き、各反復で状態n1ずつ増加します。エフェクトEffect.succeed(n)が各反復で実行され、すべての中間状態が配列に収集されます。

中間結果を集める必要がない場合は、discardオプションを使用できます。すべての中間状態を破棄し、最終結果としてundefinedを返します。

discard: true

import { Effect, Console } from "effect"
const result = Effect.loop(
1, // 初期状態
{
while: (state) => state <= 5, // ループを続ける条件,
step: (state) => state + 1, // 状態の更新関数,
body: (state) => Console.log(`Currently at state ${state}`), // 各反復で実行される効果,
discard: true
}
)
Effect.runPromise(result).then(console.log)
/*
Output:
Currently at state 1
Currently at state 2
Currently at state 3
Currently at state 4
Currently at state 5
undefined
*/

この例では、ループが各反復で現在のインデックスをログに記録するサイドエフェクトを実行しますが、すべての中間結果は破棄されます。最終結果はundefinedです。

iterate

Effect.iterate関数を使用すると、効果を持つ操作で反復することができます。効果を持つbody操作を使用して各反復中に状態を変更し、while関数がtrueと評価される限り反復を続けます:

Effect.iterate(initial, options: { while, body })

Effect.iterateはJavaScriptのwhileループに相当すると考えることができます:

let result = initial
while (options.while(result)) {
result = options.body(result)
}
return result

次の例でその動作を見てみましょう:

import { Effect } from "effect"
const result = Effect.iterate(
1, // 初期結果
{
while: (result) => result <= 5, // 反復を続ける条件
body: (result) => Effect.succeed(result + 1) // 結果を変更する操作
}
)
Effect.runPromise(result).then(console.log) // Output: 6

forEach

Effect.forEach関数は、Iterableを反復し、各要素に対して効果を持つ操作を実行できます。

forEachの構文は次のようになります:

import { Effect } from "effect"
const combinedEffect = Effect.forEach(iterable, operation, options)

指定された効果を各要素に適用します。デフォルトでは、各エフェクトは順次実行されます(これらのエフェクトの実行を管理するためのオプションについては、並行処理オプションのドキュメントを参照してください)。

この関数は、各個別のエフェクトの結果を含む配列を生成する新しいエフェクトを返します。

例を見てみましょう:

import { Effect, Console } from "effect"
const result = Effect.forEach([1, 2, 3, 4, 5], (n, index) =>
Console.log(`Currently at index ${index}`).pipe(Effect.as(n * 2))
)
Effect.runPromise(result).then(console.log)
/*
Output:
Currently at index 0
Currently at index 1
Currently at index 2
Currently at index 3
Currently at index 4
[ 2, 4, 6, 8, 10 ]
*/

この例では、配列[1, 2, 3, 4, 5]があり、各要素に対して効果を持つ操作を実行しています。出力は、操作が配列の各要素に対して実行され、現在のインデックスが表示されることを示しています。

Effect.forEachコンビネーターは、各効果の操作の結果を配列に収集するため、最終出力は[ 2, 4, 6, 8, 10 ]となります。

discardオプションも利用可能で、これがtrueに設定されていると、各効果の操作の結果が破棄されます:

import { Effect, Console } from "effect"
const result = Effect.forEach(
[1, 2, 3, 4, 5],
(n, index) =>
Console.log(`Currently at index ${index}`).pipe(Effect.as(n * 2)),
{ discard: true }
)
Effect.runPromise(result).then(console.log)
/*
Output:
Currently at index 0
Currently at index 1
Currently at index 2
Currently at index 3
Currently at index 4
undefined
*/

この場合、出力は同じですが、最終結果はundefinedになります。各エフェクト操作の結果は破棄されたためです。

all

EffectライブラリのEffect.all関数は、複数のエフェクトを1つのエフェクトにマージするための強力なツールであり、タプル、Iterable、構造体およびレコードなど、さまざまな構造化された形式で柔軟に作業できます。

allの構文は次のようになります:

import { Effect } from "effect"
const combinedEffect = Effect.all(effects, options)

ここで、effectsはマージしたい個々のエフェクトのコレクションです。

デフォルトでは、all関数は各エフェクトを順次実行します(並行処理を管理し、これらのエフェクトの実行を制御するオプションについては、並行処理オプションのドキュメントを参照してください)。

これにより、effects引数の形状に応じた形状で結果を生成する新しいエフェクトが返されます。

サポートされている各形状:タプル、Iterable、構造体、およびレコードの例を見てみましょう。

タプル

import { Effect, Console } from "effect"
const tuple = [
Effect.succeed(42).pipe(Effect.tap(Console.log)),
Effect.succeed("Hello").pipe(Effect.tap(Console.log))
] as const
const combinedEffect = Effect.all(tuple)
Effect.runPromise(combinedEffect).then(console.log)
/*
Output:
42
Hello
[ 42, 'Hello' ]
*/

Iterable

import { Effect, Console } from "effect"
const iterable: Iterable<Effect.Effect<number>> = [1, 2, 3].map((n) =>
Effect.succeed(n).pipe(Effect.tap(Console.log))
)
const combinedEffect = Effect.all(iterable)
Effect.runPromise(combinedEffect).then(console.log)
/*
Output:
1
2
3
[ 1, 2, 3 ]
*/

構造体

import { Effect, Console } from "effect"
const struct = {
a: Effect.succeed(42).pipe(Effect.tap(Console.log)),
b: Effect.succeed("Hello").pipe(Effect.tap(Console.log))
}
const combinedEffect = Effect.all(struct)
Effect.runPromise(combinedEffect).then(console.log)
/*
Output:
42
Hello
{ a: 42, b: 'Hello' }
*/

レコード

import { Effect, Console } from "effect"
const record: Record<string, Effect.Effect<number>> = {
key1: Effect.succeed(1).pipe(Effect.tap(Console.log)),
key2: Effect.succeed(2).pipe(Effect.tap(Console.log))
}
const combinedEffect = Effect.all(record)
Effect.runPromise(combinedEffect).then(console.log)
/*
Output:
1
2
{ key1: 1, key2: 2 }
*/

ショートサーキットの役割

Effect.all APIを使用するときは、エラーの管理方法を理解することが重要です。このAPIは、最初のエラーに直面すると実行をショートサーキットするように設計されています。

これは開発者にとって何を意味しますか?たとえば、順番に実行されるエフェクトのコレクションがあるとします。これらのエフェクトのいずれかが実行中にエラーを発生した場合、残りの計算はスキップされ、そのエラーが最終結果に伝播されます。

簡単に言えば、ショートサーキットの動作によって、プログラムの任意のステップで何かがうまくいかない場合には即座に停止し、エラーを返して何かがうまくいかなかったことを知らせます。

import { Effect, Console } from "effect"
const effects = [
Effect.succeed("Task1").pipe(Effect.tap(Console.log)),
Effect.fail("Task2: Oh no!").pipe(Effect.tap(Console.log)),
Effect.succeed("Task3").pipe(Effect.tap(Console.log)) // このタスクは実行されません
]
const program = Effect.all(effects)
Effect.runPromiseExit(program).then(console.log)
/*
Output:
Task1
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: 'Task2: Oh no!' }
}
*/

この動作は、modeオプションを使用することでオーバーライドできます。

modeオプション

Effect.all{ mode: "either" }オプションを使用すると、APIのエラー処理が変更されます。最初のエラーが発生したときに全体の計算をショートサーキットするのではなく、すべてのエフェクトを実行し続け、成功と失敗の両方を集計します。結果は、個々のエフェクトごとの成功した結果(Right)または失敗(Left)を表すEitherインスタンスの配列です。

以下はその例です:

import { Effect, Console } from "effect"
const effects = [
Effect.succeed("Task1").pipe(Effect.tap(Console.log)),
Effect.fail("Task2: Oh no!").pipe(Effect.tap(Console.log)),
Effect.succeed("Task3").pipe(Effect.tap(Console.log))
]
const program = Effect.all(effects, { mode: "either" })
Effect.runPromiseExit(program).then(console.log)
/*
Output:
Task1
Task3
{
_id: 'Exit',
_tag: 'Success',
value: [
{ _id: 'Either', _tag: 'Right', right: 'Task1' },
{ _id: 'Either', _tag: 'Left', left: 'Task2: Oh no!' },
{ _id: 'Either', _tag: 'Right', right: 'Task3' }
]
}
*/

一方、{ mode: "validate" }オプションをEffect.allに使用すると、{ mode: "either" }と同様のアプローチが取られますが、各エフェクトの成功または失敗を表すためにOption型を使用します。結果の配列には、成功したエフェクトにはNone、失敗したエフェクトにはその関連エラーメッセージを含むSomeが含まれます。

以下はその例です:

import { Effect, Console } from "effect"
const effects = [
Effect.succeed("Task1").pipe(Effect.tap(Console.log)),
Effect.fail("Task2: Oh no!").pipe(Effect.tap(Console.log)),
Effect.succeed("Task3").pipe(Effect.tap(Console.log))
]
const program = Effect.all(effects, { mode: "validate" })
Effect.runPromiseExit(program).then((result) => console.log("%o", result))
/*
Output:
Task1
Task3
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: [
{ _id: 'Option', _tag: 'None' },
{ _id: 'Option', _tag: 'Some', value: 'Task2: Oh no!' },
{ _id: 'Option', _tag: 'None' }
]
}
}
*/