Scopeは型安全でないので切るタイミングは注意が必要
Scope (effect)には2者の視点がある
Scope型 を導入する側
Effect.addFinalizerなどを使う側
こっちは「finalizerを追加したい」というモチベで、とりあえずそのへんのAPI使えば勝手にScope型が生えるので特に難しくない
Scope型 を排除する側
Effect.scopedなどを使う側
要するに、Scopeの(開始点と)終了点を規定する側
責務としては、
このブロックの中で使われた 全部の Scope 付きリソース は、このブロックの終わりでちゃんと片付けてほしい
ということになる
どの粒度で、Scopeを切るかをちゃんとコントールしないといけない
極論言えば、最もルート部分で解消すれば、型的には問題ないが、プログラム的には解放が遅すぎ、になりうる
以下の2つを満たしたい
A. リソースを使い終わる一番外側
なぜなら、まだ使うのに解放してしまうので
B. Aを満たす中で一番狭い部分
なぜなら、リソースを使い終わったら、なるべく早く解放したいので
例えばこういうサンプルを考える
重要なのは♠のところ
code:ts
import { Effect } from "effect";
// Effect<FakeFile, Error, Effect.Scope>
const fileResource = (path: string) =>
Effect.acquireRelease(
Effect.tryPromise({
try: () => openFile(path),
catch: (e) => new Error(failed to open file: ${String(e)}),
}),
(file) => Effect.promise(() => file.close())
);
// ♠: リソースの使用
const useFile = (path: string) =>
Effect.gen(function* () {
const file = yield* fileResource(path);
const content = yield* Effect.promise(() => file.read()); // use
console.log([useFile] content = ${content});
// release はスコープ終了時に自動で呼ばれるので気にしなくて良い
});
// ♠: 実行
const program = useFile("data2.txt").pipe(Effect.scoped);
Effect.runPromise(program);
/*
出力イメージ:
openFile opening data2.txt
FakeFile reading data2.txt
useFile content = content of data2.txt
FakeFile closing data2.txt
*/
type FakeFile = {
readonly path: string;
readonly read: () => Promise<string>;
readonly close: () => Promise<void>;
};
// 疑似的な openFile
async function openFile(path: string): Promise<FakeFile> {
return new Promise((resolve) => {
console.log([openFile] opening ${path});
resolve({
path,
read: async () => {
console.log([FakeFile] reading ${path});
return content of ${path};
},
close: async () => {
console.log([FakeFile] closing ${path});
},
});
});
}
useFileの中で、リソースの取得と使用を行っている
なので、これより小さい粒度でScopeを切るとAに違反する
例えば、こう書いた時、型エラーは起きないが、実行時エラーになる
code:ts
const useFile = (path: string) =>
Effect.gen(function* () {
const file = yield* fileResource(path).pipe(Effect.scoped); // ここでScopeを閉じる
// この時点で解放されてしまっているので、file.read()を呼ぶと実行時エラー
const content = yield* Effect.promise(() => file.read());
console.log([useFile] content = ${content});
});
// 実行
// useFileは、Effect<void, Error, never>なので、ここでEffect.scopeする必要はない
const program = useFile("data2.txt");
Effect.runPromise(program);
つまり、型安全でないmrsekut.iconmrsekut.icon*2
こういう感じで書いたほうが分かりやすそう
code:ts
const program =
Effect.gen(function* () {
// ここだけファイルを使う
yield* Effect.scoped(useFile("data.txt"));
// ここから先はもうファイルに依存しない
yield* doSomethingElse;
});