Ref
プログラムを書くとき、プログラムの実行中に何らかの状態を追跡する必要があるのは一般的です。状態とは、プログラムが実行される間に変化する可能性のあるデータを指します。例えば、カウンターアプリケーションでは、ユーザーがインクリメントまたはデクリメントすると、カウント値が変更されます。同様に、銀行アプリケーションでは、預金や引き出しが行われることで口座残高が変わります。状態管理は、インタラクティブで動的なアプリケーションを構築する上で重要です。
従来の命令型プログラミングでは、状態を保存する一般的な方法として変数を使用します。しかし、このアプローチは、状態が複数のコンポーネントや関数間で共有される場合にバグを引き起こす可能性があります。プログラムがより複雑になると、共有状態を管理するのが難しくなることがあります。
これらの問題を克服するために、Effect は可変参照を表す強力なデータ型Refを導入しています。Refを使用すると、可変変数に直接依存することなく、プログラムの異なる部分間で状態を共有できます。Refは、可変状態を制御された方法で処理し、並列環境で安全にそれを更新する道を提供します。
Effect のRefデータ型は、プログラム内の異なるファイバー間の通信を可能にします。この機能は、複数のタスクが同時に共有状態にアクセスし、更新する必要がある並行プログラミングにおいて重要です。
このガイドでは、プログラム内で状態を効果的に管理するためにRefデータ型を使用する方法を探ります。単純なカウンティングから、異なる部分間で共有される状態を持つより複雑なシナリオまでを取り上げます。さらに、並行環境でRefを使用する方法を示し、複数のタスクが安全に共有状態に相互作用できるようにします。
では、Effect プログラムにおける効果的な状態管理のためにRefを活用する方法を見ていきましょう。
Ref の使用
簡単なカウンターの例を使って、Refデータ型の使用方法を探ってみましょう。
import { Effect, Ref } from "effect"
export class Counter { inc: Effect.Effect<void> dec: Effect.Effect<void> get: Effect.Effect<number>
constructor(private value: Ref.Ref<number>) { this.inc = Ref.update(this.value, (n) => n + 1) this.dec = Ref.update(this.value, (n) => n - 1) this.get = Ref.get(this.value) }}
export const make = Effect.andThen(Ref.make(0), (value) => new Counter(value))// @include: Counter以下は、Counterの使用例です。
// @include: Counter
// @filename: index.ts// ---cut---import { Effect } from "effect";import * as Counter from "./Counter";
const program = Effect.gen(function* () { const counter = yield* Counter.make; yield* counter.inc; yield* counter.inc; yield* counter.dec; yield* counter.inc; const value = yield* counter.get; console.log(`このカウンターの値は${value}です。`);});
Effect.runPromise(program);/*出力:このカウンターの値は2です。*/// @include: Counter
// @filename: index.ts// ---cut---import { Effect, Console } from "effect";import * as Counter from "./Counter";
const program = Counter.make.pipe( Effect.andThen((counter) => counter.inc.pipe( Effect.andThen(counter.inc), Effect.andThen(counter.dec), Effect.andThen(counter.inc), Effect.andThen(counter.get), Effect.andThen((value) => Console.log(`このカウンターの値は${value}です。`) ) ) ));
Effect.runPromise(program);/*このカウンターの値は2です。*/Refデータ型上のすべての操作は効果的です。したがって、Refから読み書きする際には、効果的な操作を行っています。
並行環境での Ref の使用
RESTful API のリクエスト数をカウントするなど、並行環境でこのカウンターを使用することができます。この例では、カウンターを並行して更新してみましょう。
// @include: Counter
// @filename: index.ts// ---cut---import { Effect } from "effect";import * as Counter from "./Counter";
const program = Effect.gen(function* () { const counter = yield* Counter.make;
const logCounter = <R, E, A>(label: string, effect: Effect.Effect<A, E, R>) => Effect.gen(function* () { const value = yield* counter.get; yield* Effect.log(`${label} get: ${value}`); return yield* effect; });
yield* logCounter("タスク 1", counter.inc).pipe( Effect.zip(logCounter("タスク 2", counter.inc), { concurrent: true }), Effect.zip(logCounter("タスク 3", counter.dec), { concurrent: true }), Effect.zip(logCounter("タスク 4", counter.inc), { concurrent: true }) ); const value = yield* counter.get; yield* Effect.log(`このカウンターの値は${value}です。`);});
Effect.runPromise(program);/*出力:... fib=#2 message="タスク 4 get: 0"... fib=#4 message="タスク 3 get: 1"... fib=#5 message="タスク 1 get: 0"... fib=#5 message="タスク 2 get: 1"... fib=#0 message="このカウンターの値は2です。"*/// @include: Counter
// @filename: index.ts// ---cut---import { Effect } from "effect";import * as Counter from "./Counter";
const program = Counter.make.pipe( Effect.andThen((counter) => { const logCounter = <R, E, A>( label: string, effect: Effect.Effect<A, E, R> ) => counter.get.pipe( Effect.andThen((value) => Effect.log(`${label} get: ${value}`)), Effect.andThen(effect) );
return logCounter("タスク 1", counter.inc).pipe( Effect.zip(logCounter("タスク 2", counter.inc), { concurrent: true }), Effect.zip(logCounter("タスク 3", counter.dec), { concurrent: true }), Effect.zip(logCounter("タスク 4", counter.inc), { concurrent: true }), Effect.andThen(counter.get), Effect.andThen((value) => Effect.log(`このカウンターの値は${value}です。`) ) ); }));
Effect.runPromise(program);/*出力:... fib=#2 message="タスク 4 get: 0"... fib=#4 message="タスク 3 get: 1"... fib=#5 message="タスク 1 get: 0"... fib=#5 message="タスク 2 get: 1"... fib=#0 message="このカウンターの値は2です。"*/サービスとしての Ref の使用
状態をプログラムの異なる部分間で共有するために、Refをサービスとして渡すこともできます。この仕組みを見てみましょう。
import { Effect, Context, Ref } from "effect";
// 状態のためのタグを作成class MyState extends Context.Tag("MyState")<MyState, Ref.Ref<number>>() {}
// サブプログラム 1: 状態値を2回インクリメントconst subprogram1 = Effect.gen(function* () { const state = yield* MyState; yield* Ref.update(state, (n) => n + 1); yield* Ref.update(state, (n) => n + 1);});
// サブプログラム 2: 状態値をデクリメントした後インクリメントconst subprogram2 = Effect.gen(function* () { const state = yield* MyState; yield* Ref.update(state, (n) => n - 1); yield* Ref.update(state, (n) => n + 1);});
// サブプログラム 3: 現在の状態の値を読み取り、ログに記録const subprogram3 = Effect.gen(function* () { const state = yield* MyState; const value = yield* Ref.get(state); console.log(`MyStateの値は${value}です。`);});
// サブプログラム 1、2、および 3 を組み合わせてメインプログラムを作成const program = Effect.gen(function* () { yield* subprogram1; yield* subprogram2; yield* subprogram3;});
// 初期値0のRefインスタンスを作成const initialState = Ref.make(0);
// Refをサービスとして提供const runnable = Effect.provideServiceEffect(program, MyState, initialState);
// プログラムを実行し、出力を観察Effect.runPromise(runnable);/*出力:MyStateの値は2です。*/import { Effect, Context, Ref, Console } from "effect";
// 状態のためのタグを作成class MyState extends Context.Tag("MyState")<MyState, Ref.Ref<number>>() {}
// サブプログラム 1: 状態値を2回インクリメントconst subprogram1 = MyState.pipe( Effect.tap((state) => Ref.update(state, (n) => n + 1)), Effect.andThen((state) => Ref.update(state, (n) => n + 1)));
// サブプログラム 2: 状態値をデクリメントした後インクリメントconst subprogram2 = MyState.pipe( Effect.tap((state) => Ref.update(state, (n) => n - 1)), Effect.andThen((state) => Ref.update(state, (n) => n + 1)));
// サブプログラム 3: 現在の状態の値を読み取り、ログに記録const subprogram3 = MyState.pipe( Effect.andThen((state) => Ref.get(state)), Effect.andThen((value) => Console.log(`MyStateの値は${value}です。`)));
// サブプログラム 1、2、および 3 を組み合わせてメインプログラムを作成const program = subprogram1.pipe( Effect.andThen(subprogram2), Effect.andThen(subprogram3));
// 初期値0のRefインスタンスを作成const initialState = Ref.make(0);
// Refをサービスとして提供const runnable = Effect.provideServiceEffect(program, MyState, initialState);
// プログラムを実行し、出力を観察Effect.runPromise(runnable);/*出力:MyStateの値は2です。*/注意すべきは、Effect.provideServiceEffectを使用して、MyStateサービスの実際の実装を提供する点です。なぜなら、Refデータ型上のすべての操作は効果的であり、Ref.make(0)の作成もそれに含まれるからです。
ファイバー間での状態の共有
ユーザーからの入力から名前を読み取る例を考えてみましょう。ユーザーがコマンド"q"を入力するまで続けます。
まず、ユーザー入力を読み取るためのreadLineユーティリティを導入します(@types/nodeがインストールされていることを確認してください):
// @types: nodeimport { Effect } from "effect"import * as NodeReadLine from "node:readline"
export const readLine = ( message: string): Effect.Effect<string> => Effect.promise( () => new Promise((resolve) => { const rl = NodeReadLine.createInterface({ input: process.stdin, output: process.stdout }) rl.question(message, (answer) => { rl.close() resolve(answer) }) }) )// @include: ReadLineそれでは、メインプログラムを見てみましょう。
// @include: ReadLine
// @filename: index.ts// ---cut---import { Effect, Chunk, Ref } from "effect";import * as ReadLine from "./ReadLine";
const getNames = Effect.gen(function* () { const ref = yield* Ref.make(Chunk.empty<string>()); while (true) { const name = yield* ReadLine.readLine( "名前を入力するか、終了するには`q`を入力してください: " ); if (name === "q") { break; } yield* Ref.update(ref, (state) => Chunk.append(state, name)); } return yield* Ref.get(ref);});
Effect.runPromise(getNames).then(console.log);/*出力:名前を入力するか、終了するには`q`を入力してください: アリス名前を入力するか、終了するには`q`を入力してください: ボブ名前を入力するか、終了するには`q`を入力してください: q{ _id: "Chunk", values: [ "アリス", "ボブ" ]}*/// @include: ReadLine
// @filename: index.ts// ---cut---import { Effect, Chunk, Ref } from "effect";import * as ReadLine from "./ReadLine";
const getNames = Ref.make(Chunk.empty<string>()).pipe( Effect.andThen((ref) => ReadLine.readLine( "名前を入力するか、終了するには`q`を入力してください: " ).pipe( Effect.repeat({ while: (name) => { if (name === "q") { return Effect.succeed(false); } else { return ref.pipe( Ref.update((state) => Chunk.append(state, name)), Effect.as(true) ); } }, }), Effect.andThen(Ref.get(ref)) ) ));
Effect.runPromise(getNames).then(console.log);/*出力:名前を入力するか、終了するには`q`を入力してください: アリス名前を入力するか、終了するには`q`を入力してください: ボブ名前を入力するか、終了するには`q`を入力してください: q{ _id: "Chunk", values: [ "アリス", "ボブ" ]}*/Refデータ型の使い方を学んだ今、並行して状態を管理するために使用できます。例えば、コンソールから読み取っている間に、別のファイバーが異なるソースから状態を更新しようとしていると仮定します。
// @include: ReadLine
// @filename: index.ts// ---cut---import { Effect, Chunk, Ref, Fiber } from "effect";import * as ReadLine from "./ReadLine";
const getNames = Effect.gen(function* () { const ref = yield* Ref.make(Chunk.empty<string>()); const fiber1 = yield* Effect.fork( Effect.gen(function* () { while (true) { const name = yield* ReadLine.readLine( "名前を入力するか、終了するには`q`を入力してください: " ); if (name === "q") { break; } yield* Ref.update(ref, (state) => Chunk.append(state, name)); } }) ); const fiber2 = yield* Effect.fork( Effect.gen(function* () { for (const name of ["ジョン", "ジェーン", "ジョー", "トム"]) { yield* Ref.update(ref, (state) => Chunk.append(state, name)); yield* Effect.sleep("1秒"); } }) ); yield* Fiber.join(fiber1); yield* Fiber.join(fiber2); return yield* Ref.get(ref);});
Effect.runPromise(getNames).then(console.log);/*出力:名前を入力するか、終了するには`q`を入力してください: アリス名前を入力するか、終了するには`q`を入力してください: ボブ名前を入力するか、終了するには`q`を入力してください: q{ _id: "Chunk", values: [ ... ]}*/// @include: ReadLine
// @filename: index.ts// ---cut---import { Effect, Chunk, Ref, Fiber } from "effect";import * as ReadLine from "./ReadLine";
const getNames = Ref.make(Chunk.empty<string>()).pipe( Effect.andThen((ref) => { const fiber1 = ReadLine.readLine( "名前を入力するか、終了するには`q`を入力してください: " ).pipe( Effect.repeat({ while: (name) => { if (name === "q") { return Effect.succeed(false); } else { return ref.pipe( Ref.update((state) => Chunk.append(state, name)), Effect.as(true) ); } }, }), Effect.fork ); const fiber2 = Effect.fork( Effect.forEach( ["ジョン", "ジェーン", "ジョー", "トム"], (name) => ref.pipe( Ref.update((state) => Chunk.append(state, name)), Effect.andThen(Effect.sleep("1秒")) ), { concurrency: "unbounded", discard: true } ) ); return Effect.all([fiber1, fiber2]).pipe( Effect.andThen(([f1, f2]) => Fiber.join(f1).pipe(Effect.andThen(Fiber.join(f2))) ), Effect.andThen(Ref.get(ref)) ); }));
Effect.runPromise(getNames).then(console.log);/*出力:名前を入力するか、終了するには`q`を入力してください: アリス名前を入力するか、終了するには`q`を入力してください: ボブ名前を入力するか、終了するには`q`を入力してください: q{ _id: "Chunk", values: [ ... ]}*/