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];
});

これでだいぶ治安が良くなった。