Skip to content

Instantly share code, notes, and snippets.

@ykst
Created January 28, 2015 14:07
Show Gist options
  • Save ykst/d76add675fb6c417aede to your computer and use it in GitHub Desktop.
Save ykst/d76add675fb6c417aede to your computer and use it in GitHub Desktop.
method_missingと内部DSL

method_missingと内部DSL

Rubyの魅力の一つとして一見外部DSLのような内部DSLを書けてしまうというものがありますが、 method_missingを効果的に使うことで更に外部言語っぽい記述を可能にするという芸が有ります。 (という事を以前とある忍者に教えて貰い、別件で改めて使ってみてその効果を実感したので共有してみます。)

例えばこんなDSL

まず平凡なDSLとして、次のようなものを考えてみます。

struct(:data) {
  int :x;
  int :y;
};

intstructをメソッドとして持つクラスでこのDSLをevalで読み込んでパースする、というのが通常の流れです。 簡単に構文木に変換するだけのメソッドは次のようなものになります。

class Parser
  def initialize
    @env_stack = [[]]
  end

  def parse(program)
    eval(program)
    p @env_stack
  end

  private

    def int(name)
      var = { :id => name, :type => :int }
      @env_stack.last.push var
    end

    def struct(name)
      var = { :id => name, :type => :struct }
      @env_stack.push []
      yield if block_given?
      var.merge! :fields => @env_stack.pop
      @env_stack.last.push var
    end
end

実行結果は以下のようになります。

Parser.new.parse(%Q(
struct(:data) {
  int :x;
  int :y;
}
))

=>
[[ 
  {
    :id => :data, :type => :struct,
    :fields => [
      { :id => :x, :type => :int },
      { :id => :y, :type => :int },
    ]
  }
]]

このようにして平文から構文木が取れれば後はいくらでも応用技を掛けていく事が出来ます。これが内部DSLの基本です。 しかしこのDSLはシンボルを変数として使用しているため、見た目も書き味も今一つイケていません。 技が極まった時のDSLはエンジン実装よりも遥かに多く書かれるため、書き味に手間隙を掛けるのはとても大切です。

method_missingでシンボルを定義する

そこでmethod_missingを使用します。

def method_missing(name, *args)
  name
end

このようにする事で、未定義メソッドのシンボルを変数名としてintstruct等の述語に渡す事が出来るようになり、 DSLは次のような親しみやすいシンタックスになります。

struct(data) {
  int x;
  int y;
};

実際これだけだとあまり面白くないのですが、例えばmethod_missingの中で環境を走査して定義や型をチェックしたり、 スタックに積んで後段に渡したりといった様々なsemantic actionを変数を参照した時点で発動させる事で、 非常に柔軟かつ強力なDSLエンジンが実装出来るようになります。 欠点としてはDSLをタイポした時でもmethod_missingが発動するので、 しばしば不可解なエラーが出てデバッグがしにくくなるというものがありますが、そこはまあ、頑張りましょう。

外部DSL vs. 内部DSL

外部DSLを導入する際には比較的強い理由が求められると思いますが、 その多くは自由なシンタックスが欲しいという単純な動機に基くものです。 しかし、いくら自由が良いからと言ってもある程度慣れ親しんだシンタックスに依拠する場合、 特にPascal風やC風のシンタックスが欲しいだけなら、 わざわざスクラッチでパーサーを書かずとも上述のテクニックで見た目外部DSLっぽいRuby文を定義することが出来ます。 無論内部DSLなので、必要に迫られたときにいつでもRubyネイティブの機能を導入する事が出来るのも強みです。 今回紹介したように、比較的自由なシンタックスと強力な内部言語機能をmethod_missingで橋渡しする事で、 手早くそれっぽいDSLフレームワークを構築することが出来るようになります。

@ykst
Copy link
Author

ykst commented Sep 27, 2015

内部DSLという文脈は使う人によって若干異なる(特にrubyの場合は)ため、ここで行った整理は的を射ていないかもしれない。いずれにせよ、内部DSLの形態を特定のデザインパターンに落とし込むのは難しいだろう。ライブラリとは須らく内部DSLである。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment