過剰なネストを簡素化する
エフェクトが実行されるのにかかる時間を印刷するカスタム関数 elapsed を作成したいとします。
単純なパイプを使用する
最初は、標準の pipe メソッド を使用したコードが思いつくかもしれませんが、このアプローチは過剰なネストを引き起こし、冗長で読みづらいコードになる可能性があります。
import { Effect, Console } from "effect"
// 現在のタイムスタンプを取得const now = Effect.sync(() => new Date().getTime())
// `self` を実行するのにかかった時間を印刷const elapsed = <R, E, A>( self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => now.pipe( Effect.andThen((startMillis) => self.pipe( Effect.andThen((result) => now.pipe( Effect.andThen((endMillis) => { // ミリ秒単位で経過時間を計算 const elapsed = endMillis - startMillis // 経過時間をログに記録 return Console.log(`Elapsed: ${elapsed}`).pipe( Effect.map(() => result) ) }) ) ) ) ) )
// 200ミリ秒の遅延で成功する計算をシミュレートconst task = Effect.succeed("some task").pipe(Effect.delay("200 millis"))
const program = elapsed(task)
Effect.runPromise(program).then(console.log)/*Output:Elapsed: 204some task*/この問題に対処し、コードをより管理しやすくする解決策があります。それが「doシミュレーション」です。
「doシミュレーション」を使用する
Effect の「doシミュレーション」は、他のプログラミング言語の「do記法」に似た、より宣言的なスタイルでコードを書くことを可能にします。これにより、Effect.bind や Effect.let のような関数を使用して、変数を定義し、それらに操作を行うことができます。
doシミュレーションの使い方は以下の通りです。
-
Effect.Do値を使用して doシミュレーションを開始します:const program = Effect.Do.pipe(/* ... 残りのコード */) -
doシミュレーションのスコープ内で、
Effect.bind関数を使用して変数を定義し、それらをEffect値にバインドできます:Effect.bind("variableName", (scope) => effectValue)
variableNameは、定義したい変数の名前です。スコープ内で一意である必要があります。effectValueは、変数にバインドしたいEffect値です。関数呼び出しの結果や他の有効なEffect値である必要があります。
-
複数の
Effect.bindステートメントを累積して、スコープ内で複数の変数を定義できます:Effect.bind("variable1", () => effectValue1),Effect.bind("variable2", ({ variable1 }) => effectValue2),// ... 追加のバインドステートメント -
doシミュレーションのスコープ内でも、
Effect.let関数を使用して変数を定義し、それらを単純な値にバインドできます:Effect.let("variableName", (scope) => simpleValue)
variableNameは、変数に付ける名前です。先ほどと同様に、スコープ内で一意である必要があります。simpleValueは、変数に割り当てたい値です。number、string、またはbooleanのような単純な値である必要があります。
-
Effect.andThen、Effect.flatMap、Effect.tap、Effect.mapなどの通常のEffect関数は、doシミュレーション内でも引き続き使用できます。これらの関数は、スコープ内で累積された変数を引数として受け取ります:Effect.andThen(({ variable1, variable2 }) => {// variable1 と variable2 を使用して操作を行う// 結果として `Effect` 値を返す})
doシミュレーションを用いることで、elapsed コンビネーターを次のように書き換えることができます:
import { Effect, Console } from "effect"
// 現在のタイムスタンプを取得const now = Effect.sync(() => new Date().getTime())
const elapsed = <R, E, A>( self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => Effect.Do.pipe( Effect.bind("startMillis", () => now), Effect.bind("result", () => self), Effect.bind("endMillis", () => now), Effect.let( "elapsed", ({ startMillis, endMillis }) => endMillis - startMillis // ミリ秒単位で経過時間を計算 ), Effect.tap(({ elapsed }) => Console.log(`Elapsed: ${elapsed}`)), // 経過時間をログに記録 Effect.map(({ result }) => result) )
// 200ミリ秒の遅延で成功する計算をシミュレートconst task = Effect.succeed("some task").pipe(Effect.delay("200 millis"))
const program = elapsed(task)
Effect.runPromise(program).then(console.log)/*Output:Elapsed: 204some task*/このソリューションでは、doシミュレーションを使用してコードが簡素化されました。elapsed 関数は Effect.Do から始まり、シミュレーションスコープに入ります。
スコープ内では、Effect.bind を使用して変数を定義し、それらを対応するエフェクトにバインドしています。
Effect.gen を使用する
最も簡潔で便利な解決策は、Effect.gen コンストラクタを使用することです。これにより、エフェクトを扱う際に ジェネレータ を使用できます。このアプローチは、ジェネレータ構文が提供するネイティブスコープを活用し、過剰なネストを回避し、より簡潔なコードを実現します。
import { Effect } from "effect"
// 現在のタイムスタンプを取得const now = Effect.sync(() => new Date().getTime())
// `self` を実行するのにかかった時間を印刷const elapsed = <R, E, A>( self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => Effect.gen(function* () { const startMillis = yield* now const result = yield* self const endMillis = yield* now // ミリ秒単位で経過時間を計算 const elapsed = endMillis - startMillis // 経過時間をログに記録 console.log(`Elapsed: ${elapsed}`) return result })
// 200ミリ秒の遅延で成功する計算をシミュレートconst task = Effect.succeed("some task").pipe(Effect.delay("200 millis"))
const program = elapsed(task)
Effect.runPromise(program).then(console.log)/*Output:Elapsed: 204some task*/このソリューションでは、コードを簡素化するために ジェネレータ を使用することに切り替えています。elapsed 関数は現在、実行フローを定義するためにジェネレータ関数 (Effect.gen) を使用しています。ジェネレータ内では、yield* を使用してエフェクトを呼び出し、その結果を変数にバインドします。これにより、ネストが排除され、より読みやすく、直線的なコード構造が提供されます。
Effectにおけるジェネレータスタイルは、より線形で逐次的な実行フローを使用しており、従来の命令型プログラミング言語に似ています。これにより、コードが読みやすく、理解しやすくなります。特に、命令型プログラミングパラダイムに慣れている開発者にとって理解しやすいです。
対照的に、パイプスタイルは、特に複雑なエフェクトのある計算を扱う際に過剰なネストを引き起こす可能性があります。これにより、コードが追跡しにくくなり、デバッグが難しくなることがあります。
Effectにおけるジェネレータの使い方についての詳細は、Effectにおけるジェネレータの使用ガイドを参照してください。