RubyでのDSLの作り方
Jay Fields' Thoughts: Implementing an internal DSL in Ruby
RubyでのDSLの作り方をexpectationsというtesting frameworkを例にとって説明している。
やっぱassert_equalってオブジェクト指向っぽくなくてカッコ悪いよねwここはDSLでカッコよく書きたいものだ。
彼の言いたいことを日本語で超要約してみる。
まず、どういうふうに書きたいかを決める。こんな感じにしたい。
Expectations do # テストだよというブロック expect :expected do # :expectedが期待される値 :expected # ブロックの評価結果が実際の値 end end
これから、これをRubyスクリプトとして実行可能な形式にする。
まず、DSLを読み込んだら(実行したら)、Expectations::Suiteオブジェクトが作成されるかを確認する。
require 'singleton' module Expectations; end class Expectations::Suite include Singleton end def Expectations(&block) Expectations::Suite.instance end Expectations do expect :expected do :expected end end Expectations::Suite.instance # => #<Expectations::Suite:0x1d4fc>
よし、たしかにできている。次に、DSLを実行したらExpectations::Suiteに格納されるExpectationの個数を確認する。
require 'singleton' module Expectations; end class Expectations::Suite include Singleton attr_accessor :expectations def initialize self.expectations = [] end def expect(expected, &actual) expectations << :expectation # ここは仮実装! end end def Expectations(&block) Expectations::Suite.instance.instance_eval(&block) end Expectations do expect :expected do :expected end end Expectations::Suite.instance # => #<Expectations::Suite:0x1c82c @expectations=[:expectation]> Expectations::Suite.instance.expectations.size # => 1
最後にSuiteに格納するExpectationを実際に作成し、expectedとactualの値が等しいことを確認する。
# (略) class Expectations::Suite # (略) def expect(expected, &actual) expectations << Expectations::Expectation.new(expected, actual) end end # 構造体に毛が生えたようなクラス class Expectations::Expectation attr_accessor :expected, :actual def initialize(expected, actual) self.expected, self.actual = expected, actual.call end end def Expectations(&block) Expectations::Suite.instance.instance_eval(&block) end Expectations do expect :expected do :expected end end Expectations::Suite.instance Expectations::Suite.instance.expectations.size # => 1 Expectations::Suite.instance.expectations.first.expected # => :expected Expectations::Suite.instance.expectations.first.actual # => :expected
suiteが実行されるまでブロックの評価を遅延するなどまだやることはあるけど、open class、クラスメソッド、evalのおかげでRubyでDSLを作成するのは慣れれば簡単だ。
本物のexpectationsは「sudo gem install expectations」でインストールして確かめてくれ。
訳注
とはいえ腕に覚えのある人でないと難しいと思うが…
あと、&でブロックを丸投げできることにも触れててほしいな。地味に便利だから。