JavaScript
DSL
メタプログラミング

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

はじめに

エンジニアになってからフレームワークは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: