例えばこんな感じでスッキリとしません
const fn = () => {
// なにか1
let foo
try {
foo = createFoo()
// なにか2
} finally {
destroyFoo(foo)
}
// なにか3
}
どうにかしたいと思ってこういうものを作りました
const fina = (fn) => {
const fins = []
const use = (value, fin) => {
fins.push(() => fin(value))
return value
}
return Promise.resolve(use).then(fn).finally(() => {
for (const fin of fins) fin()
})
}
fina(use => {
const foo = use(createFoo(), foo => destroyFoo(foo))
console.log(foo)
state.running = true
use(null, () => state.running = false)
})
fina 関数に関数を渡して実行します
引数で受け取る use 関数で finally の処理をセットします
リソースの確保と同時に解放時の処理を書けるので普通の try-finally よりスッキリとして見やすくなります
1 つめの引数に渡した値は use 自体の結果として受け取れます
また 2 つめの引数である finally で呼び出される関数の引数としても渡されます
同じスコープなら外側が見れるので単純に
fina(addFinally => {
const foo = createFoo()
addFinally(() => destroyFoo(foo))
console.log(foo)
})
というインターフェースでもよかったのですが use の引数として取得と解放がセットになるし 変数名を気にせず引数で受け取った名前で解放できるほうがいいかなと思ってこうしました
finally の用途は解放が必要なリソースに限らず 外部の変数の状態を書き換えて 抜ける場合はエラーでも必ず戻すというときにも使います
そういうときは 1 つめの引数は null などにして 2 つめの引数で戻す処理を書きます
1 つめの引数も finally の処理と対称になるように関数にすれば 外部の変数を書き換えたりする処理でも自然と use の引数にかけてまとまるとも思いましたが その場で呼び出すだけだし 関数にするほどでもないかなと思って非対称になってます
基本こういう処理はだいたい非同期処理が入るだろうと思って非同期前提の Promise を使った処理になっています
ただこういう同期処理(⇩例)の場合もあって 非同期化すると使う側にも伝播するので同期版もほしいなと思って同期・非同期両対応版も作りました
fina(use => {
const url = use(URL.createObjectURL(blob), url => URL.revokeObjectURL(url))
console.log(url)
})
const xsyncFinally = (fn, fin) => {
let handled = false
let ret
try {
const maybe_promise = fn()
if (maybe_promise instanceof Promise) {
ret = maybe_promise.finally(() => {
fin()
})
handled = true
} else {
ret = maybe_promise
}
return ret
} finally {
if (!handled) {
fin()
}
}
}
const fina = (fn) => {
const fins = []
const use = (value, fin) => {
fins.push(() => fin(value))
return value
}
return xsyncFinally(
() => fn(use),
() => { for (const fin of fins) fin() }
)
}
使う側はさっきと一緒です