Rubyの魅力の一つとして一見外部DSLのような内部DSLを書けてしまうというものがありますが、
method_missingを効果的に使うことで更に外部言語っぽい記述を可能にするという芸が有ります。
(という事を以前とある忍者に教えて貰い、別件で改めて使ってみてその効果を実感したので共有してみます。)
まず平凡なDSLとして、次のようなものを考えてみます。
struct(:data) {
int :x;
int :y;
};intやstructをメソッドとして持つクラスでこの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を使用します。
def method_missing(name, *args)
name
endこのようにする事で、未定義メソッドのシンボルを変数名としてintやstruct等の述語に渡す事が出来るようになり、
DSLは次のような親しみやすいシンタックスになります。
struct(data) {
int x;
int y;
};実際これだけだとあまり面白くないのですが、例えばmethod_missingの中で環境を走査して定義や型をチェックしたり、
スタックに積んで後段に渡したりといった様々なsemantic actionを変数を参照した時点で発動させる事で、
非常に柔軟かつ強力なDSLエンジンが実装出来るようになります。
欠点としてはDSLをタイポした時でもmethod_missingが発動するので、
しばしば不可解なエラーが出てデバッグがしにくくなるというものがありますが、そこはまあ、頑張りましょう。
外部DSLを導入する際には比較的強い理由が求められると思いますが、
その多くは自由なシンタックスが欲しいという単純な動機に基くものです。
しかし、いくら自由が良いからと言ってもある程度慣れ親しんだシンタックスに依拠する場合、
特にPascal風やC風のシンタックスが欲しいだけなら、
わざわざスクラッチでパーサーを書かずとも上述のテクニックで見た目外部DSLっぽいRuby文を定義することが出来ます。
無論内部DSLなので、必要に迫られたときにいつでもRubyネイティブの機能を導入する事が出来るのも強みです。
今回紹介したように、比較的自由なシンタックスと強力な内部言語機能をmethod_missingで橋渡しする事で、
手早くそれっぽいDSLフレームワークを構築することが出来るようになります。
内部DSLという文脈は使う人によって若干異なる(特にrubyの場合は)ため、ここで行った整理は的を射ていないかもしれない。いずれにせよ、内部DSLの形態を特定のデザインパターンに落とし込むのは難しいだろう。ライブラリとは須らく内部DSLである。