2023/9/20 using : リソース変数の管理が楽になるキーワード (JavaScript&TypeScript)
この記事は2023/9/20に書きました。
この記事の内容にある意見は、個人の主観的意見を前提とします。
記事の内容は間違いがあり得ますので、ご了承いただけると幸いです。内容の間違い、認識の違い、違う意見などありましたら、コメント大歓迎です!
概要
JavaScriptにおいて、変数を初期化できるkeyword var, let, const
に加え、using
がまもなく追加される予定です。
using
キーワードで宣言した変数は、リソースタイプ変数として見なされ、変数がscope外になる前に自動でリソース解除作業を行うことができるようになり、従来の明示的にリソース解除コードを書くという面倒な作業がだいぶ楽になることが期待できます。
ECMAScript Proposalは以下となっておりStage3段階となってます。The TC39 Processをご参考ください。
※ Stage4になると標準として確定されます。詳しくはまた、TypeScript 5.2ではusing
keywordが利用可能です。
※ただ、現在はpolyfillコードが必要だったりするようです。
この記事では、以下の内容を紹介します。
- 基本的な使い方
- 従来の課題と解決ポイント
- TypeScript 5.2での動作方法とコード例
Sample Code
TypeScript 5.2での動作コード例は、以下のgithub repositoryにてご参考いただけます。
背景
ファイル操作、データベース操作、ネットワーク通信(Socket)など、外部リソースを扱う場合、リソースのopenとclose(expose & dispose)を明示的にハンドリングする必要があります。
結論、上記のようなリソースの明示的なハンドリングは、プログラマーにおいて意外と負担がかかるという課題がありました。その課題を解決するために、using
keywordが提案され、もうすぐ標準仕様として確定となる予定です。
例えば、node-postgres
を利用したPostgreSQLの操作において、connect()
とclose()
を明示的に処理する必要があり、close()
をしない場合はDB Connectionのリソース解除がされない状態からプロセスが終了しない問題などに起きたりします。
JavaのAutoClosable
など、他の言語ではすでにこの問題のための機能が備わりつつありましたが、JavaScriptでは正式な機能はなかったのが現状でした。
そのため、プログラマーがいろいろ書き方を工夫して行なっていたのが従来の方法であり、以下のようにtry-finally
で行うのがよくあるパターンと思います。
// ... in async function
let client: Client;
try {
client = new Client({...connectionInfo});
await client.connect();
//... some logic
} catch (e) {
//... some error handling
} finally {
await client.close();
}
しかし、この方法でも、使うポイントごとにtry-finally
を書くとか、それを避けたく、Wrapper Classにて自動解除されるようなものを作ったりなど、いろいろ課題はありました。
一番大きいな問題は、チーム開発において、こちらを共通認識として一貫性を保ったコードの維持が、なかなか難しかったという課題を個人的には持っておりました。
基本的な構文
以下のように、Symbol.dispose
をSymbol keyとして、リソース解除処理のcallback関数として明示的に定義し、using
keywordでリソース変数を宣言して使う形になります。
using
として宣言されたリソース変数に対しては、「呼び出すscope上での明示的にリソース解除」は不要となります。
リソース変数がscope外になる直前(callstack終了の直前)に、自動的にSymbol.dispose
として定義していた関数が呼ばれ、リソース解除処理が行われるようになります。
const getResource = () => {
//... 利用したいリソースの初期化コードを作成
return {
//usingとして使われる変数は、Symbol.disposeというsymbol keyをもち、
//resourceをdisposeする処理を行うcallback関数を持つ
[Symbol.dispose]: () => {
//... リソース解除ロジックを作成
},
};
};
function doWorkOnResource() {
// using keywordを使いリソース変数を宣言
using resource = getResource();
//... some logic
return;
// using変数がスコープ外になると、自動的にdisposeが遂行される
}
async/awaitのように、非同期処理の動機化をしたい場合は、Symbol.asyncDispose
を使う必要があります。
const getResource = () =>
//...
[Symbol.asyncDispose]: async () => {
//... write to dispose resource with await
},
//...
async function doWorkOnResource() {
await using resource = await getResource();
}
また、classで実装する場合はDisposable
,AsyncDisposable
というinterfaceをimplementsし実装することも可能です。
class SomeResource implements Dispoable {
//...
async [Symbol.dispose]() {
//... write to dispose resource
}
class SomeResource implements AsyncDisposable {
//...
async [Symbol.asyncDispose]() {
//... write to dispose resource with await
}
ユースケース
ここでは、node-postgres
を利用し、PostgreSQL
にconnectしselectを行った後、closeを行う例を紹介します。
サンプルコードは、以下のgithub repositoryからご参考いただけます。
pg-try-finally.example.ts
従来のtry-catch-finally
を使ったリソースハンドリングの例です。
import * as dotenv from "dotenv";
dotenv.config();
import { Client } from "pg";
/**
* @throws Error : errors from pg
*/
const isAvailableDBConnection = async () => {
let client: Client;
try {
//try to connect
client = new Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: +process.env.DB_PORT,
});
await client.connect();
//query
const res = await client.query("SELECT $1::text as message", [
"Hello world!",
]);
console.info(res.rows[0].message); // Hello world!
//return true if query is successful
return true;
} catch (e) {
console.error(e);
throw e;
} finally {
console.debug("try to client.end()");
await client.end();
}
};
isAvailableDBConnection();
pg-asyncDispose-func.example.ts
usingとasyncDisposeを使った例となります。
AsyncDisposable Functionとして作成しています。
import * as dotenv from "dotenv";
dotenv.config();
import { Client } from "pg";
// Because dispose and asyncDispose are so new, we need to manually 'polyfill' the existence of these functions in order for TypeScript to use them
// See: https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#using-declarations-and-explicit-resource-management
(Symbol as any).dispose ??= Symbol("Symbol.dispose");
(Symbol as any).asyncDispose ??= Symbol("Symbol.asyncDispose");
/**
* @returns : {client: pg.Client, [Symbol.asyncDispose] : dispose function}
*/
const getDBConnection = async () => {
//try to connect
const client = new Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: +process.env.DB_PORT,
});
await client.connect();
//return resource as disposable
return {
client,
[Symbol.asyncDispose]: async () => {
console.debug("try to client.end()");
await client.end();
},
};
};
/**
* @throws Error : errors from pg
*/
const isAvailableDBConnection = async () => {
// declear resource variable by `using keyword`
await using db = await getDBConnection();
const res = await db.client.query("SELECT $1::text as message", [
"Hello world!",
]);
console.info(res.rows[0].message);
// ...
// before out of scope, resource will be disposed by function of [Symbol.asyncDispose]
};
isAvailableDBConnection();
pg-asyncDispose-class.example.ts
usingとasyncDisposeを使った例となります。
AsyncDisposable Classとして作成しています。
import * as dotenv from "dotenv";
dotenv.config();
import { Client } from "pg";
// Because dispose and asyncDispose are so new, we need to manually 'polyfill' the existence of these functions in order for TypeScript to use them
// See: https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#using-declarations-and-explicit-resource-management
(Symbol as any).dispose ??= Symbol("Symbol.dispose");
(Symbol as any).asyncDispose ??= Symbol("Symbol.asyncDispose");
/**
* disposable class
*/
class DBConnection implements AsyncDisposable {
private client: Client;
private constructor() {
this.client = new Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: +process.env.DB_PORT,
});
}
/**
* factory method
* @returns : new instance of DBConnection
*/
static async of() {
const instance = new DBConnection();
await instance.connect();
return instance;
}
/**
* implement by asyncDisposable interface
*/
async [Symbol.asyncDispose]() {
console.debug("try to client.end()");
await this.client.end();
}
getClient() {
return this.client;
}
private async connect() {
await this.client.connect();
}
}
/**
* @throws Error : errors from pg
*/
const isAvailableDBConnection = async () => {
// declear resource variable by `using keyword`
await using db = await DBConnection.of();
const client = db.getClient()
const res = await client.query("SELECT $1::text as message", [
"Hello world!",
]);
console.info(res.rows[0].message);
// ...
// before out of scope, resource will be disposed by function of [Symbol.asyncDispose]
};
isAvailableDBConnection();
後書き
using
keywordがもたらす一番のメリットは「標準化」と思います。
もちろんusing
キーワードが上記の問題を全てを解決してくれるわけではないと思いますが、かなり改善を望めると思います。
私の経験としては、チーム開発において「リソース管理のためのコード」が、メンバーによって差があったり、漏れていたりする問題で、ランタイム時にぶつかるややこしい問題に直面したことが多くあります。
最近は、そのためDI(Dependency Injection)が可能なframeworkを仕入れ、外部リソースをSingletonの生命周期、またはmethod levelの生命周期を意識して、必要に応じて共通化した後、ミドルウェアとして、チーム全体で意識を合わせたあと、開発をしており、今後もそうしようと思っておりました。
ただ、こちらもこちらで、初期開発や共通認識化において難しい点はあると感じます。
ORMをよく採用したい理由としても、他の理由もいろいろありますが、上記の理由も一つではあります。
また、外部リソースはDBだけではないため、外部リソース変数の管理は昔から常に課題でした。
そこで、using
という標準機能が用意されることで、JavaScript, TypeScriptを扱うプログラマーを自然的にこの知識を身につけるようになることで、チーム開発においてのリソース管理における品質維持も楽になると期待しています。
Discussion