Node.jsのcallbackスタイルAPIをPromiseに変換するHaxe macroが書けた
執筆時のバージョン情報: Haxe 3.4.6
Node.jsのcallbackスタイルAPIを毎回手でPromiseに変換するのがダルすぎる。
最初はNode.jsの util.promisify()
を使おうかなと思ってたんだけど、Promiseを返す関数に変換するだけで、そのままcallしてくれるわけではないのと、型がどうしても付けづらいのでやめた。次の方法として、マクロでどうにかならないか試していたら、思いのほか良いものができた。
import haxe.macro.Expr; import haxe.macro.Context; class PromiseTools { public static macro function callAsPromise<T>( fn: Expr, ?params: ExprOf<Array<Dynamic>>, ?cb: ExprOf<Array<Dynamic> -> T>): ExprOf<js.Promise<T>> { var args = (if (isNull(params)) { []; } else { switch (params.expr) { case ExprDef.EArrayDecl(exprs): exprs; case _: Context.error("params must be EArrayDecl", params.pos); } }).concat([ if (isNull(cb)) { macro function (error, result) { if (untyped error) { reject(error); } else { // workaround for `Error -> Void -> Void` callback. resolve(untyped result); } } } else { macro untyped function (error) { if (untyped error) { reject(error); } else { var data = untyped __js__("Array.from(arguments).slice(1)"); resolve((${cb})(data)); } } } ]); return macro new js.Promise(function (resolve, reject) { $e{ {expr: ExprDef.ECall(fn, args), pos: fn.pos} }; }); } static function isNull(expr: Expr): Bool { return switch (expr.expr) { case ExprDef.EConst(CIdent("null")): true; case _: false; } } }
usingを使えば、割とすっきりとしたコードになる。
import js.node.Fs; using PromiseTools; class Main { static function main() { Fs.readFile.callAsPromise(["test.txt"]).then(function (data) { trace(data); }); } }
コールバックが Error -> T -> Void
ではなく、Error -> T -> Dynamic -> Void
みたいな引数が2個を超える変形パターン(Cosmos DBのNode.js SDKがこんな感じ)にも対応できた。
// var client: DocumentClient = ...; client.readDatabase.callAsPromise([DATABASE_URI], function (ret): Database { trace(ret[0]); trace(ret[1]); return ret[0]; });
これでだいぶ治安が良くなった。