Edited at

JavascriptでDSL作ってメタプログラミングしたい

More than 1 year has passed since last update.


はじめに

エンジニアになってからフレームワークはRails一筋で通していましたが、最近 nodeに魂を売り始めた @alfaです。

しかし、Javascriptを書いていて一つ気になることがあるのです。

Rubyをこよなく愛すエンジニアの皆さんは、感じたことがあるのではないでしょうか?そう「DSLを気軽に定義できない」んです!! :sob:

今回はJavascriptでどうしてもDSLを作りたかったお話です。


一体何がやりたいのか

Javascriptのgetter/setterの定義、めっちゃ面倒ですよね?

class Hoge {

get hoge() {
return this._hoge;
}

set hoge(v) {
this._hoge = v;
}

get fuga() {
return this._fuga;
}

set fuga(v) {
this._fuga = v;
}
}

例えば Ruby で言う attr_acessor を作りたい!!

class Hoge {

attrAcessor('hoge');
attrAcessor('fuga');
}

3秒後に反射的に書いたコード

class AttributeAccessor {

attrAcessor(name) {
Object.defineProperty(this, name, {
get: function() { return this[`_${name}`] },
set: function(v) { this[`_${name}`] = v },
});
}
}

あとは、extend…、いや、ん?… :innocent:

そもそも言語仕様的にRubyのようなDSLは作れないんですね。

でもRubyみたいに手軽にDSLを実装したい!


妥協案 (不採用)

関数内にDSLを書く感じに作ればいいか

class Base {

constructor() {
this.constructor.definer(this);
}

attrAcessor(name) {
// 略
}
}

class Hoge extends Base {
static definer(d) {
d.attrAccessor('hoge');
}
}

と妥協したものの...

class Hoge extends Base {

//↓これ嫌い
static definer(d) {
d.attrAccessor('hoge');
//↑これ嫌い
}
}

なんだかイマイチでした。


和解案

最終的に下記のような形で和解しました。

class Hoge extends Base {

static definer() {
attrAccessor('hoge');
}
}

さて、みなさんは上記のDSLをどのように実現したか想像がつくでしょうか?

ちなみに、下記のコードでは実現できません。

class Hoge extends Base {

constructor() {
this.constructor.definer.bind(this)();
}
}

なぜなら attrAccessor の前に this がないからです。


実現方法

:cat: eval と契約してDSLおじさんになってよ

const scripts = [];

scripts.push(`const attrAcessor = this.attrAcessor`);

scripts.push(this._getFunctionBody(this.constructor.definer));

eval(scripts.join(';'));


  1. DSLの命令を定義

  2. 定義(definer)関数の内部を抽出


  3. 1. 2. を結合し eval で評価

(eval の直接的呼び出しはローカルスコープになるため this.attrAcessor の参照が可能です。)

※ あくまでも概念の説明用です、thisを束縛していないため動作しません。

※ 実際の実装はこちら


命令の定義までevalしている理由

ローカルスコープが参照できるなら、命令の定義まで eval する必要はないと考えるかもしれません。

const attrAcessor = this.attrAcessor; 

eval(this._getFunctionBody(this.constructor.definer));

しかし、上記のコードでは最適化やminifyされた場合に、変数名が圧縮されたり、そもそも削られてしまうかもしれません。(と言うかされます)

その対策として、命令の定義も eval しています。


一応ライブラリにしています

需要あるかわかりませんが、自分で使うので… track-dsl

こんな感じで使用します。

const DSL = require('track-dsl');

class Base {
constructor() {
const dsl = new DSL(this, {
'attrAccessor': {func: this._attrAcessor, binding: this},
});

dsl.evaluate(this.constructor.definer);
}

_attrAcessor(name) {
Object.defineProperty(this, name, {
get: function() { return this[`_${name}`] },
set: function(v) { this[`_${name}`] = v },
});
}
}

class Hoge extends Base {

static definer() {
attrAcessor('hoge');
attrAcessor('fuga');
}
}

hoge = new Hoge();

hoge.hoge
hoge.fuga

以上です。

もっと良い方法の提案、ライブラリへのPRお待ちしています!!! :bow: