LoginSignup
6
7

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-02-10

はじめに

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

6
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7