Scopeの理解
code:ts
const file = openFile("foo.txt") // ①リソースを取得して、
const content = file.read() // ②なんか操作して
file.close() // ③リソースを解放する
実際は、①が複雑だったり、②の途中で失敗したり、例外・キャンセルが起きたりする
リソースを提供する側としては、下記2つをひとまとめの単位として提供したい
①リソース取得
③リソース解放
しかし一方で、
③「いつ解放すべきか」はリソース使用者が決めるという事情がある
そこで理想的な責務分離はこうなる
リソース取得側は、後始末の方法だけを公開する
後始末をいつ実行するかは、呼び出し側が決める
code:ts
// ① scopeの開始
const file = openFile("foo.txt") // a1: リソースの取得
const content = file.read() // a2: リソースの利用
file.close() // a3: リソースの解放
// ② scopeの終了
スコープの定義 ①②
リソースの取得と解放
a1,a3をセットで行う高レベルなAPI
a3のみ
リソース使用 a2
本来のビジネスロジック
準備としてのopenFileの定義
詳細は完全に無視して良いmrsekut.icon
code:ts
type FakeFile = {
readonly path: string
readonly read: () => Promise<string>
readonly close: () => Promise<void>
}
// 疑似的な openFile
const openFile = (path: string): Promise<FakeFile> =>
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})
}
})
})
例えば、ダミーのファイルリソースは以下のように書ける
code:ts
import { Effect } from "effect"
// Effect<FakeFile, Error, Effect.Scope>
const fileResource = (path: string) =>
Effect.acquireRelease(
// a1: ファイルを開く
Effect.tryPromise({
try: () => openFile(path),
catch: (e) => new Error(failed to open file: ${String(e)})
}),
// a3: Scope が閉じられたときに close する
(file) => Effect.promise(() => file.close())
)
リソースの取得と解放をひとまとめにしたeffectを定義している
いつその後始末を実行するかは自分では決めない
そのため、この Effect の型にはEffect<_, _, Scope>のように Scope が現れる
これは以下のように解釈できるmrsekut.icon
私は finalizer を登録したので、必ず Scope の中で実行されなければならない
もっと言うと、「どこかでscopeを切って、リソースの解放タイミングを明示せよ」ということ
クソデカscopeだと、解放されないことになるので無意味なためmrsekut.icon
上のリソースを利用する側はこう書く
code:ts
// a2: ファイルリソースを使って「なんか操作する」処理を書く
const useFile = (path: string) =>
Effect.gen(function* () {
const file = yield* fileResource(path) // acquire
const content = yield* Effect.promise(() => file.read()) // use
console.log([useFile] content = ${content})
// release はスコープ終了時に自動で呼ばれるので気にしなくて良い
})
Effect.scoped は Scope の lifetime を外側から明示する
code:ts
const runnable = Effect.scoped(useFile("data.txt"))
内部でやっていること
1. 新しい Scope を開始する
2. useFile をその Scope の中で実行する
3. Scope を閉じる前に、finalizerを逆順で実行する
4. Scope を閉じる
code:ts
// 実行例
const program = Effect.scoped(useFile("data2.txt"))
Effect.runPromise(program)
/*
出力イメージ:
useFile content = content of data2.txt */
本質的な価値は次の点にある
Scope の終了点を、呼び出し側が明確に決められること
= いつ後始末を確実に実行すべきかを明らかにできる